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)/layout.tsx b/src/app/[locale]/(marketing)/layout.tsx index f68e025..210ca22 100644 --- a/src/app/[locale]/(marketing)/layout.tsx +++ b/src/app/[locale]/(marketing)/layout.tsx @@ -1,7 +1,9 @@ import { getTranslations, setRequestLocale } from 'next-intl/server'; -import Link from 'next/link'; import { DemoBanner } from '@/components/DemoBanner'; import { LocaleSwitcher } from '@/components/LocaleSwitcher'; +import { NavLink } from '@/components/NavLink'; +import { marketingNavigation } from '@/config/navigation'; +import { styles } from '@/config/styles'; import { BaseTemplate } from '@/templates/BaseTemplate'; export default async function Layout(props: { @@ -21,67 +23,24 @@ export default async function Layout(props: { -
  • - - {t('home_link')} - -
  • -
  • - - {t('about_link')} - -
  • -
  • - - {t('counter_link')} - -
  • -
  • - - {t('portfolio_link')} - -
  • -
  • - - GitHub - -
  • + {marketingNavigation.left.map(link => ( +
  • + + {link.label || t(link.translationKey)} + +
  • + ))} )} rightNav={( <> -
  • - - {t('sign_in_link')} - -
  • - -
  • - - {t('sign_up_link')} - -
  • + {marketingNavigation.right.map(link => ( +
  • + + {t(link.translationKey)} + +
  • + ))}
  • @@ -89,7 +48,7 @@ export default async function Layout(props: { )} > -
    {props.children}
    +
    {props.children}
    ); 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/NavLink.tsx b/src/components/NavLink.tsx new file mode 100644 index 0000000..f231163 --- /dev/null +++ b/src/components/NavLink.tsx @@ -0,0 +1,34 @@ +import Link from 'next/link'; +import { styles } from '@/config/styles'; + +type NavLinkProps = { + href: string; + children: React.ReactNode; + external?: boolean; +}; + +/** + * Navigation link component with consistent styling + * Used for navigation menus in layouts + */ +export function NavLink({ href, children, external }: NavLinkProps) { + if (external) { + return ( + + {children} + + ); + } + + return ( + + {children} + + ); +} diff --git a/src/components/SponsorSection.tsx b/src/components/SponsorSection.tsx new file mode 100644 index 0000000..cb79b51 --- /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 && sponsor.logo.width > 0 && sponsor.logo.height > 0) + .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/navigation.ts b/src/config/navigation.ts new file mode 100644 index 0000000..fedf69a --- /dev/null +++ b/src/config/navigation.ts @@ -0,0 +1,61 @@ +/** + * Navigation configuration for marketing layout + * Defines navigation items for left and right navigation menus + */ + +export type NavLink = { + id: string; + translationKey: string; + href: string; + external?: boolean; + label?: string; // For links without translation (like GitHub) +}; + +export type NavigationConfig = { + left: NavLink[]; + right: NavLink[]; +}; + +export const marketingNavigation: NavigationConfig = { + left: [ + { + id: 'home', + translationKey: 'home_link', + href: '/', + }, + { + id: 'about', + translationKey: 'about_link', + href: '/about/', + }, + { + id: 'counter', + translationKey: 'counter_link', + href: '/counter/', + }, + { + id: 'portfolio', + translationKey: 'portfolio_link', + href: '/portfolio/', + }, + { + id: 'github', + label: 'GitHub', + translationKey: '', + href: 'https://github.com/ixartz/Next-js-Boilerplate', + external: true, + }, + ], + right: [ + { + id: 'sign-in', + translationKey: 'sign_in_link', + href: '/sign-in/', + }, + { + id: 'sign-up', + translationKey: 'sign_up_link', + href: '/sign-up/', + }, + ], +}; diff --git a/src/config/sponsors.ts b/src/config/sponsors.ts new file mode 100644 index 0000000..c4f18cc --- /dev/null +++ b/src/config/sponsors.ts @@ -0,0 +1,85 @@ +/** + * 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', + }, + ], + '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..2105a50 --- /dev/null +++ b/src/config/styles.ts @@ -0,0 +1,34 @@ +/** + * 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', + nav: 'border-none text-gray-700 hover:text-gray-900', + }, + 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', + }, + containers: { + contentPadding: 'py-5 text-xl [&_p]:my-6', + }, +} as const;