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')}
-
-
-
-
-
+
>
);
};
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() {
-
-
-
-
-
+
>
);
};
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')}
-
-
-
-
-
+
>
);
};
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
-
-
-
-
-
-
+
>
);
};
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 => (
+
+
+
+ ))}
+ >
+ );
+}
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;