Refactor marketing pages to use config-driven components and styles

Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-01-08 16:02:03 +00:00
parent 6e78f0f582
commit 37a0de6e4a
9 changed files with 275 additions and 120 deletions

View File

@@ -1,6 +1,7 @@
import type { Metadata } from 'next';
import { getTranslations, setRequestLocale } from 'next-intl/server';
import Image from 'next/image';
import { SponsorSection } from '@/components/SponsorSection';
import { sponsors } from '@/config/sponsors';
type IAboutProps = {
params: Promise<{ locale: string }>;
@@ -31,25 +32,7 @@ export default async function About(props: IAboutProps) {
<>
<p>{t('about_paragraph')}</p>
<div className="mt-2 text-center text-sm">
{`${t('translation_powered_by')} `}
<a
className="text-blue-700 hover:border-b-2 hover:border-blue-700"
href="https://l.crowdin.com/next-js"
>
Crowdin
</a>
</div>
<a href="https://l.crowdin.com/next-js">
<Image
className="mx-auto mt-2"
src="/assets/images/crowdin-dark.png"
alt="Crowdin Translation Management System"
width={128}
height={26}
/>
</a>
<SponsorSection sponsors={sponsors.about} namespace="About" />
</>
);
};

View File

@@ -1,9 +1,10 @@
import type { Metadata } from 'next';
import { useTranslations } from 'next-intl';
import { getTranslations } from 'next-intl/server';
import Image from 'next/image';
import { CounterForm } from '@/components/CounterForm';
import { CurrentCount } from '@/components/CurrentCount';
import { SponsorSection } from '@/components/SponsorSection';
import { sponsors } from '@/config/sponsors';
export async function generateMetadata(props: {
params: Promise<{ locale: string }>;
@@ -31,27 +32,7 @@ export default function Counter() {
<CurrentCount />
</div>
<div className="mt-5 text-center text-sm">
{`${t('security_powered_by')} `}
<a
className="text-blue-700 hover:border-b-2 hover:border-blue-700"
href="https://launch.arcjet.com/Q6eLbRE"
>
Arcjet
</a>
</div>
<a
href="https://launch.arcjet.com/Q6eLbRE"
>
<Image
className="mx-auto mt-2"
src="/assets/images/arcjet-light.svg"
alt="Arcjet"
width={128}
height={38}
/>
</a>
<SponsorSection sponsors={sponsors.counter} namespace="Counter" />
</>
);
};

View File

@@ -1,6 +1,8 @@
import type { Metadata } from 'next';
import { getTranslations, setRequestLocale } from 'next-intl/server';
import { Sponsors } from '@/components/Sponsors';
import { StyledLink } from '@/components/StyledLink';
import { styles } from '@/config/styles';
type IIndexProps = {
params: Promise<{ locale: string }>;
@@ -31,20 +33,19 @@ export default async function Index(props: IIndexProps) {
<>
<p>
{`Follow `}
<a
className="text-blue-700 hover:border-b-2 hover:border-blue-700"
<StyledLink
href="https://twitter.com/ixartz"
target="_blank"
rel="noreferrer noopener"
>
@Ixartz on Twitter
</a>
</StyledLink>
{` for updates and more information about the boilerplate.`}
</p>
<h2 className="mt-5 text-2xl font-bold">
<h2 className={styles.headings.h2Bold}>
Boilerplate Code for Your Next.js Project with Tailwind CSS
</h2>
<p className="text-base">
<p className={styles.text.base}>
Next.js Boilerplate is a developer-friendly starter code for Next.js projects, built with Tailwind CSS and TypeScript.
{' '}
<span role="img" aria-label="zap">
@@ -53,19 +54,19 @@ export default async function Index(props: IIndexProps) {
{' '}
Designed with developer experience in mind, it includes:
</p>
<ul className="mt-3 text-base">
<ul className={styles.lists.baseMarginTop}>
<li>🚀 Next.js with App Router support</li>
<li>🔥 TypeScript for type checking</li>
<li>💎 Tailwind CSS integration</li>
<li>
🔒 Authentication with
{' '}
<a
className="font-bold text-blue-700 hover:border-b-2 hover:border-blue-700"
<StyledLink
href="https://clerk.com?utm_source=github&amp;utm_medium=sponsorship&amp;utm_campaign=nextjs-boilerplate"
variant="primaryBold"
>
Clerk
</a>
</StyledLink>
{' '}
(includes passwordless, social, and multi-factor auth)
</li>
@@ -76,12 +77,12 @@ export default async function Index(props: IIndexProps) {
<li>
🌐 Multi-language support (i18n) with next-intl and
{' '}
<a
className="font-bold text-blue-700 hover:border-b-2 hover:border-blue-700"
<StyledLink
href="https://l.crowdin.com/next-js"
variant="primaryBold"
>
Crowdin
</a>
</StyledLink>
</li>
<li>🔴 Form handling (React Hook Form) and validation (Zod)</li>
<li>📏 Linting and formatting (ESLint, Prettier)</li>
@@ -91,43 +92,43 @@ export default async function Index(props: IIndexProps) {
<li>
🐰 AI-powered code reviews with
{' '}
<a
className="font-bold text-blue-700 hover:border-b-2 hover:border-blue-700"
<StyledLink
href="https://www.coderabbit.ai?utm_source=next_js_starter&utm_medium=github&utm_campaign=next_js_starter_oss_2025"
variant="primaryBold"
>
CodeRabbit
</a>
</StyledLink>
</li>
<li>
🚨 Error monitoring (
<a
className="font-bold text-blue-700 hover:border-b-2 hover:border-blue-700"
<StyledLink
href="https://sentry.io/for/nextjs/?utm_source=github&amp;utm_medium=paid-community&amp;utm_campaign=general-fy25q1-nextjs&amp;utm_content=github-banner-nextjsboilerplate-logo"
variant="primaryBold"
>
Sentry
</a>
</StyledLink>
) and logging (LogTape, an alternative to Pino.js)
</li>
<li>🖥 Monitoring as Code (Checkly)</li>
<li>
🔐 Security and bot protection (
<a
className="font-bold text-blue-700 hover:border-b-2 hover:border-blue-700"
<StyledLink
href="https://launch.arcjet.com/Q6eLbRE"
variant="primaryBold"
>
Arcjet
</a>
</StyledLink>
)
</li>
<li>🤖 SEO optimization (metadata, JSON-LD, Open Graph tags)</li>
<li> Development tools (VSCode config, bundler analyzer, changelog generation)</li>
</ul>
<p className="text-base">
<p className={styles.text.base}>
Our sponsors&apos; exceptional support has made this project possible.
Their services integrate seamlessly with the boilerplate, and we
recommend trying them out.
</p>
<h2 className="mt-5 text-2xl font-bold">{t('sponsors_title')}</h2>
<h2 className={styles.headings.h2Bold}>{t('sponsors_title')}</h2>
<Sponsors />
</>
);

View File

@@ -1,6 +1,7 @@
import type { Metadata } from 'next';
import { getTranslations, setRequestLocale } from 'next-intl/server';
import Image from 'next/image';
import { SponsorSection } from '@/components/SponsorSection';
import { sponsors } from '@/config/sponsors';
import { routing } from '@/libs/I18nRouting';
type IPortfolioDetailProps = {
@@ -44,27 +45,7 @@ export default async function PortfolioDetail(props: IPortfolioDetailProps) {
<h1 className="capitalize">{t('header', { slug })}</h1>
<p>{t('content')}</p>
<div className="mt-5 text-center text-sm">
{`${t('code_review_powered_by')} `}
<a
className="text-blue-700 hover:border-b-2 hover:border-blue-700"
href="https://www.coderabbit.ai?utm_source=next_js_starter&utm_medium=github&utm_campaign=next_js_starter_oss_2025"
>
CodeRabbit
</a>
</div>
<a
href="https://www.coderabbit.ai?utm_source=next_js_starter&utm_medium=github&utm_campaign=next_js_starter_oss_2025"
>
<Image
className="mx-auto mt-2"
src="/assets/images/coderabbit-logo-light.svg"
alt="CodeRabbit"
width={128}
height={22}
/>
</a>
<SponsorSection sponsors={sponsors['portfolio-slug']} namespace="PortfolioSlug" />
</>
);
};

View File

@@ -1,7 +1,8 @@
import type { Metadata } from 'next';
import { getTranslations, setRequestLocale } from 'next-intl/server';
import Image from 'next/image';
import Link from 'next/link';
import { StyledLink } from '@/components/StyledLink';
import { SponsorSection } from '@/components/SponsorSection';
import { sponsors } from '@/config/sponsors';
type IPortfolioProps = {
params: Promise<{ locale: string }>;
@@ -34,44 +35,17 @@ export default async function Portfolio(props: IPortfolioProps) {
<div className="grid grid-cols-1 justify-items-start gap-3 md:grid-cols-2 xl:grid-cols-3">
{Array.from(Array.from({ length: 6 }).keys()).map(elt => (
<Link
className="hover:text-blue-700"
<StyledLink
variant="hoverBlue"
key={elt}
href={`/portfolio/${elt}`}
>
{t('portfolio_name', { name: elt })}
</Link>
</StyledLink>
))}
</div>
<div className="mt-5 text-center text-sm">
{`${t('error_reporting_powered_by')} `}
<a
className="text-blue-700 hover:border-b-2 hover:border-blue-700"
href="https://sentry.io/for/nextjs/?utm_source=github&utm_medium=paid-community&utm_campaign=general-fy25q1-nextjs&utm_content=github-banner-nextjsboilerplate-logo"
>
Sentry
</a>
{` - ${t('coverage_powered_by')} `}
<a
className="text-blue-700 hover:border-b-2 hover:border-blue-700"
href="https://about.codecov.io/codecov-free-trial/?utm_source=github&utm_medium=paid-community&utm_campaign=general-fy25q1-nextjs&utm_content=github-banner-nextjsboilerplate-logo"
>
Codecov
</a>
</div>
<a
href="https://sentry.io/for/nextjs/?utm_source=github&utm_medium=paid-community&utm_campaign=general-fy25q1-nextjs&utm_content=github-banner-nextjsboilerplate-logo"
>
<Image
className="mx-auto mt-2"
src="/assets/images/sentry-dark.png"
alt="Sentry"
width={128}
height={38}
/>
</a>
<SponsorSection sponsors={sponsors.portfolio} namespace="Portfolio" />
</>
);
};

View File

@@ -0,0 +1,53 @@
import { useTranslations } from 'next-intl';
import Image from 'next/image';
import type { SponsorConfig } from '@/config/sponsors';
import { styles } from '@/config/styles';
type SponsorSectionProps = {
sponsors: SponsorConfig[];
namespace: string;
};
export function SponsorSection({ sponsors, namespace }: SponsorSectionProps) {
const t = useTranslations(namespace);
if (!sponsors || sponsors.length === 0) {
return null;
}
return (
<>
<div className={`${styles.spacing.marginTop5} ${styles.text.centerSmall}`}>
{sponsors.map((sponsor, index) => (
<span key={sponsor.id}>
{index > 0 && ' - '}
{`${t(sponsor.translationKey)} `}
<a
className={styles.links.primary}
href={sponsor.url}
>
{sponsor.name}
</a>
</span>
))}
</div>
{sponsors
.filter(sponsor => sponsor.logo.src)
.map(sponsor => (
<a
key={sponsor.id}
href={sponsor.url}
>
<Image
className={styles.image.centerMarginTop}
src={sponsor.logo.src}
alt={sponsor.logo.alt}
width={sponsor.logo.width}
height={sponsor.logo.height}
/>
</a>
))}
</>
);
}

View File

@@ -0,0 +1,54 @@
import Link from 'next/link';
import { styles } from '@/config/styles';
type StyledLinkProps = {
href: string;
children: React.ReactNode;
variant?: 'primary' | 'primaryBold' | 'hoverBlue';
target?: '_blank' | '_self' | '_parent' | '_top';
rel?: string;
className?: string;
};
/**
* Styled Link component that uses configured styles
* Provides consistent link styling across the app
*/
export function StyledLink({
href,
children,
variant = 'primary',
target,
rel,
className,
}: StyledLinkProps) {
const baseClassName = styles.links[variant];
const combinedClassName = className ? `${baseClassName} ${className}` : baseClassName;
// Use Next.js Link for internal links, regular <a> for external
const isExternal = href.startsWith('http') || href.startsWith('//');
if (isExternal) {
return (
<a
className={combinedClassName}
href={href}
target={target}
rel={rel}
>
{children}
</a>
);
}
return (
<Link
className={combinedClassName}
href={href}
target={target}
rel={rel}
>
{children}
</Link>
);
}

98
src/config/sponsors.ts Normal file
View File

@@ -0,0 +1,98 @@
/**
* Sponsors configuration for marketing pages
* Defines sponsor sections that appear at the bottom of marketing pages
*/
export type SponsorConfig = {
id: string;
name: string;
description: string;
url: string;
logo: {
src: string;
alt: string;
width: number;
height: number;
};
translationKey: string;
};
export type PageSponsorsConfig = {
[page: string]: SponsorConfig[];
};
export const sponsors: PageSponsorsConfig = {
about: [
{
id: 'crowdin',
name: 'Crowdin',
description: 'Translation Management System',
url: 'https://l.crowdin.com/next-js',
logo: {
src: '/assets/images/crowdin-dark.png',
alt: 'Crowdin Translation Management System',
width: 128,
height: 26,
},
translationKey: 'translation_powered_by',
},
],
portfolio: [
{
id: 'sentry',
name: 'Sentry',
description: 'Error Monitoring',
url: 'https://sentry.io/for/nextjs/?utm_source=github&utm_medium=paid-community&utm_campaign=general-fy25q1-nextjs&utm_content=github-banner-nextjsboilerplate-logo',
logo: {
src: '/assets/images/sentry-dark.png',
alt: 'Sentry',
width: 128,
height: 38,
},
translationKey: 'error_reporting_powered_by',
},
{
id: 'codecov',
name: 'Codecov',
description: 'Code Coverage',
url: 'https://about.codecov.io/codecov-free-trial/?utm_source=github&utm_medium=paid-community&utm_campaign=general-fy25q1-nextjs&utm_content=github-banner-nextjsboilerplate-logo',
logo: {
src: '',
alt: 'Codecov',
width: 0,
height: 0,
},
translationKey: 'coverage_powered_by',
},
],
'portfolio-slug': [
{
id: 'coderabbit',
name: 'CodeRabbit',
description: 'AI Code Reviews',
url: 'https://www.coderabbit.ai?utm_source=next_js_starter&utm_medium=github&utm_campaign=next_js_starter_oss_2025',
logo: {
src: '/assets/images/coderabbit-logo-light.svg',
alt: 'CodeRabbit',
width: 128,
height: 22,
},
translationKey: 'code_review_powered_by',
},
],
counter: [
{
id: 'arcjet',
name: 'Arcjet',
description: 'Security and Bot Protection',
url: 'https://launch.arcjet.com/Q6eLbRE',
logo: {
src: '/assets/images/arcjet-light.svg',
alt: 'Arcjet',
width: 128,
height: 38,
},
translationKey: 'security_powered_by',
},
],
};

30
src/config/styles.ts Normal file
View File

@@ -0,0 +1,30 @@
/**
* Shared styles configuration
* Defines reusable CSS class names for consistent styling across the app
*/
export const styles = {
links: {
primary: 'text-blue-700 hover:border-b-2 hover:border-blue-700',
primaryBold: 'font-bold text-blue-700 hover:border-b-2 hover:border-blue-700',
hoverBlue: 'hover:text-blue-700',
},
text: {
centerSmall: 'text-center text-sm',
base: 'text-base',
},
spacing: {
marginTop2: 'mt-2',
marginTop3: 'mt-3',
marginTop5: 'mt-5',
},
image: {
centerMarginTop: 'mx-auto mt-2',
},
headings: {
h2Bold: 'mt-5 text-2xl font-bold',
},
lists: {
baseMarginTop: 'mt-3 text-base',
},
} as const;