diff --git a/src/app/[locale]/(marketing)/about/page.tsx b/src/app/[locale]/(marketing)/about/page.tsx index 376bf85..c503239 100644 --- a/src/app/[locale]/(marketing)/about/page.tsx +++ b/src/app/[locale]/(marketing)/about/page.tsx @@ -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) { <>

{t('about_paragraph')}

-
- {`${t('translation_powered_by')} `} - - Crowdin - -
- - - Crowdin Translation Management System - + ); }; diff --git a/src/app/[locale]/(marketing)/counter/page.tsx b/src/app/[locale]/(marketing)/counter/page.tsx index 78ba126..0f47560 100644 --- a/src/app/[locale]/(marketing)/counter/page.tsx +++ b/src/app/[locale]/(marketing)/counter/page.tsx @@ -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() { -
- {`${t('security_powered_by')} `} - - Arcjet - -
- - - Arcjet - + ); }; diff --git a/src/app/[locale]/(marketing)/page.tsx b/src/app/[locale]/(marketing)/page.tsx index a907767..fbfa0c9 100644 --- a/src/app/[locale]/(marketing)/page.tsx +++ b/src/app/[locale]/(marketing)/page.tsx @@ -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) { <>

{`Follow `} - @Ixartz on Twitter - + {` for updates and more information about the boilerplate.`}

-

+

Boilerplate Code for Your Next.js Project with Tailwind CSS

-

+

Next.js Boilerplate is a developer-friendly starter code for Next.js projects, built with Tailwind CSS and TypeScript. {' '} @@ -53,19 +54,19 @@ export default async function Index(props: IIndexProps) { {' '} Designed with developer experience in mind, it includes:

-
    +
    • 🚀 Next.js with App Router support
    • 🔥 TypeScript for type checking
    • 💎 Tailwind CSS integration
    • 🔒 Authentication with {' '} - Clerk - + {' '} (includes passwordless, social, and multi-factor auth)
    • @@ -76,12 +77,12 @@ export default async function Index(props: IIndexProps) {
    • 🌐 Multi-language support (i18n) with next-intl and {' '} - Crowdin - +
    • 🔴 Form handling (React Hook Form) and validation (Zod)
    • 📏 Linting and formatting (ESLint, Prettier)
    • @@ -91,43 +92,43 @@ export default async function Index(props: IIndexProps) {
    • 🐰 AI-powered code reviews with {' '} - CodeRabbit - +
    • 🚨 Error monitoring ( - Sentry - + ) and logging (LogTape, an alternative to Pino.js)
    • 🖥️ Monitoring as Code (Checkly)
    • 🔐 Security and bot protection ( - Arcjet - + )
    • 🤖 SEO optimization (metadata, JSON-LD, Open Graph tags)
    • ⚙️ Development tools (VSCode config, bundler analyzer, changelog generation)
    -

    +

    Our sponsors' exceptional support has made this project possible. Their services integrate seamlessly with the boilerplate, and we recommend trying them out.

    -

    {t('sponsors_title')}

    +

    {t('sponsors_title')}

    ); diff --git a/src/app/[locale]/(marketing)/portfolio/[slug]/page.tsx b/src/app/[locale]/(marketing)/portfolio/[slug]/page.tsx index e4d2448..0ed0d33 100644 --- a/src/app/[locale]/(marketing)/portfolio/[slug]/page.tsx +++ b/src/app/[locale]/(marketing)/portfolio/[slug]/page.tsx @@ -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) {

    {t('header', { slug })}

    {t('content')}

    -
    - {`${t('code_review_powered_by')} `} - - CodeRabbit - -
    - - - CodeRabbit - + ); }; diff --git a/src/app/[locale]/(marketing)/portfolio/page.tsx b/src/app/[locale]/(marketing)/portfolio/page.tsx index 196d842..cac6993 100644 --- a/src/app/[locale]/(marketing)/portfolio/page.tsx +++ b/src/app/[locale]/(marketing)/portfolio/page.tsx @@ -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) {
    {Array.from(Array.from({ length: 6 }).keys()).map(elt => ( - {t('portfolio_name', { name: elt })} - + ))}
    -
    - {`${t('error_reporting_powered_by')} `} - - Sentry - - {` - ${t('coverage_powered_by')} `} - - Codecov - -
    - - - Sentry - + ); }; diff --git a/src/components/SponsorSection.tsx b/src/components/SponsorSection.tsx new file mode 100644 index 0000000..637b0d5 --- /dev/null +++ b/src/components/SponsorSection.tsx @@ -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 ( + <> +
    + {sponsors.map((sponsor, index) => ( + + {index > 0 && ' - '} + {`${t(sponsor.translationKey)} `} + + {sponsor.name} + + + ))} +
    + + {sponsors + .filter(sponsor => sponsor.logo.src) + .map(sponsor => ( + + {sponsor.logo.alt} + + ))} + + ); +} diff --git a/src/components/StyledLink.tsx b/src/components/StyledLink.tsx new file mode 100644 index 0000000..1f5565a --- /dev/null +++ b/src/components/StyledLink.tsx @@ -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 for external + const isExternal = href.startsWith('http') || href.startsWith('//'); + + if (isExternal) { + return ( + + {children} + + ); + } + + return ( + + {children} + + ); +} diff --git a/src/config/sponsors.ts b/src/config/sponsors.ts new file mode 100644 index 0000000..e3f95bc --- /dev/null +++ b/src/config/sponsors.ts @@ -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', + }, + ], +}; diff --git a/src/config/styles.ts b/src/config/styles.ts new file mode 100644 index 0000000..de61b24 --- /dev/null +++ b/src/config/styles.ts @@ -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;