From 464cec51125dbd0204ea9aaf6a23235550401aa2 Mon Sep 17 00:00:00 2001 From: karbivskyi Date: Thu, 16 Apr 2026 06:02:15 +0300 Subject: [PATCH] codex texts update --- next-sitemap.config.js | 2 +- next.config.js | 5 + package.json | 2 +- src/app/[locale]/contact/page.tsx | 14 +- src/app/[locale]/guides/[slug]/page.tsx | 84 +- src/app/[locale]/guides/page.tsx | 30 +- src/app/[locale]/layout.tsx | 88 +- src/app/[locale]/page.tsx | 38 +- src/app/page.tsx | 18 +- src/app/robots.ts | 15 + src/app/sitemap.ts | 44 + src/components/Breadcrumbs.tsx | 51 +- src/components/Footer.jsx | 175 +- src/components/HomePageSections.tsx | 539 +++++++ src/components/HtmlLangSync.tsx | 13 + src/components/JsonLd.tsx | 14 + src/components/LocalizedPageShell.tsx | 6 +- src/content/articles.ts | 1943 ++++++++++++++++++++++- src/lib/guides-ui.ts | 96 ++ src/lib/seo.ts | 219 +++ src/lib/site-config.ts | 82 + src/lib/static-pages-i18n.ts | 186 +-- 22 files changed, 3257 insertions(+), 407 deletions(-) create mode 100644 src/app/robots.ts create mode 100644 src/app/sitemap.ts create mode 100644 src/components/HomePageSections.tsx create mode 100644 src/components/HtmlLangSync.tsx create mode 100644 src/components/JsonLd.tsx create mode 100644 src/lib/seo.ts create mode 100644 src/lib/site-config.ts diff --git a/next-sitemap.config.js b/next-sitemap.config.js index b25fb00..c04e6de 100644 --- a/next-sitemap.config.js +++ b/next-sitemap.config.js @@ -31,7 +31,7 @@ module.exports = { changefreq: 'weekly', priority: 0.7, sitemapSize: 5000, - exclude: ['/server-sitemap.xml'], + exclude: ['/', '/server-sitemap.xml'], robotsTxtOptions: { policies: [ { diff --git a/next.config.js b/next.config.js index 88fcf84..d77c33f 100644 --- a/next.config.js +++ b/next.config.js @@ -26,6 +26,11 @@ const nextConfig = { }, async redirects() { return [ + { + source: '/en', + destination: '/', + permanent: true, + }, { source: '/:path*', has: [ diff --git a/package.json b/package.json index fa420e5..ebe74da 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "start": "next start", "dev": "next dev", "build": "next build", - "postbuild": "next-sitemap", + "postbuild": "node -e \"console.log('sitemap and robots are handled by the Next.js metadata routes')\"", "db:apply-schema": "node scripts/apply-content-schema.mjs", "db:seed-content": "node scripts/seed-content.mjs", "db:seed-content:mongo": "node scripts/seed-content-mongo.mjs", diff --git a/src/app/[locale]/contact/page.tsx b/src/app/[locale]/contact/page.tsx index e3b961f..4ef3c70 100644 --- a/src/app/[locale]/contact/page.tsx +++ b/src/app/[locale]/contact/page.tsx @@ -1,6 +1,5 @@ import type { Metadata } from 'next'; import Breadcrumbs from '@/components/Breadcrumbs'; -import ContactCaptcha from '@/components/ContactCaptcha'; import LocalizedPageShell from '@/components/LocalizedPageShell'; import { getStaticPageCopy, STATIC_PAGE_LOCALES } from '@/lib/static-pages-i18n'; @@ -39,13 +38,18 @@ export default async function ContactPage({ params }: { params: Promise<{ locale { label: copy.contact }, ]} /> -
+

{copy.contactTitle}

{copy.contactIntro}

-
- +
+

+ {copy.contactEmailLabel}: + + {contactEmail} + +

+

{copy.contactResponseLabel}

-

{copy.contactResponseLabel}

diff --git a/src/app/[locale]/guides/[slug]/page.tsx b/src/app/[locale]/guides/[slug]/page.tsx index d1e2059..a2665a7 100644 --- a/src/app/[locale]/guides/[slug]/page.tsx +++ b/src/app/[locale]/guides/[slug]/page.tsx @@ -22,9 +22,10 @@ import { } from '@/lib/guides-interlink-plan'; import { getLocalizedInterlinkAnchor } from '@/lib/interlinking-anchors'; import { getPlacementLabel } from '@/lib/interlinking-placements'; +import JsonLd from '@/components/JsonLd'; +import { BASE_URL, buildLocaleAlternates, getLocaleHomePath, normalizeLocale } from '@/lib/site-config'; +import { buildGuideArticleSchema } from '@/lib/seo'; -const baseUrl = 'https://www.generatepasswordto.me'; -const hreflangMap: Record = { ua: 'uk' }; export const runtime = 'nodejs'; export const revalidate = 3600; @@ -46,7 +47,8 @@ export async function generateMetadata({ return {}; } - const canonical = `${baseUrl}/${locale}/guides/${slug}`; + const normalizedLocale = normalizeLocale(locale); + const canonical = `${BASE_URL}/${normalizedLocale}/guides/${slug}`; return { title: guide.seoTitle, @@ -54,9 +56,7 @@ export async function generateMetadata({ keywords: guide.keywords, alternates: { canonical, - languages: Object.fromEntries( - SUPPORTED_CONTENT_LANGUAGES.map((lang) => [hreflangMap[lang] || lang, `${baseUrl}/${lang}/guides/${slug}`]) - ), + languages: buildLocaleAlternates(`${normalizedLocale}/guides/${slug}`), }, openGraph: { title: guide.seoTitle, @@ -130,13 +130,29 @@ export default async function GuideArticlePage({ const introEntries = interlinkEntries.filter((entry) => entry.placement === 'intro'); const bodyEntries = interlinkEntries.filter((entry) => entry.placement === 'body'); const conclusionEntries = interlinkEntries.filter((entry) => entry.placement === 'conclusion'); + const articleSections = guide.sections.map((section, index) => ({ + id: `section-${index + 1}`, + heading: section.heading, + })); + const normalizedLocale = normalizeLocale(locale); + const canonical = `${BASE_URL}/${normalizedLocale}/guides/${slug}`; + const articleSchema = buildGuideArticleSchema({ + locale: normalizedLocale, + title: guide.seoTitle, + description: guide.seoDescription, + url: canonical, + image: guide.image.startsWith('http') ? guide.image : `${BASE_URL}${guide.image}`, + dateModified: guide.updatedAt, + author: guide.author, + }); return ( +
{guide.title} -

- {renderGuideTextWithLinks(guide.excerpt, linkTargets, interlinkState, 'excerpt')} -

+
+
+
+

+ {uiCopy.articleSummaryLabel} +

+

+ {renderGuideTextWithLinks(guide.excerpt, linkTargets, interlinkState, 'excerpt')} +

+
+
+ +
@@ -172,7 +213,7 @@ export default async function GuideArticlePage({
{guide.sections.map((section, index) => ( -
+

{section.heading}

{section.paragraphs.map((paragraph, pIndex) => ( @@ -195,6 +236,25 @@ export default async function GuideArticlePage({ ))} +
+

+ {uiCopy.nextStepsLabel} +

+
+ + {uiCopy.backToGuidesLabel} + + + {uiCopy.contactLabel} + +
+
diff --git a/src/app/[locale]/guides/page.tsx b/src/app/[locale]/guides/page.tsx index 14c04b8..4615e89 100644 --- a/src/app/[locale]/guides/page.tsx +++ b/src/app/[locale]/guides/page.tsx @@ -7,22 +7,20 @@ import { formatGuideDate, getGuidesUiCopy } from '@/lib/guides-ui'; import LocalizedPageShell from '@/components/LocalizedPageShell'; import Breadcrumbs from '@/components/Breadcrumbs'; import GuideReadButton from '@/components/GuideReadButton'; +import { buildGuideCollectionMetadata, getLocaleLabel } from '@/lib/seo'; +import { getLocaleHomePath } from '@/lib/site-config'; -const baseUrl = 'https://www.generatepasswordto.me'; export const runtime = 'nodejs'; export const revalidate = 3600; export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise { const { locale } = await params; - const canonical = `${baseUrl}/${locale}/guides`; - - return { - title: 'Password Security Guides', - description: 'In-depth guides about password security, compliance, and account protection best practices.', - alternates: { - canonical, - }, - }; + const localeLabel = getLocaleLabel(locale); + return buildGuideCollectionMetadata( + locale, + `Password security guides in ${localeLabel} | generatepasswordto.me`, + 'Explore practical guides about strong passwords, password policies, incident response, and modern authentication hygiene.', + ); } export default async function GuidesIndexPage({ params }: { params: Promise<{ locale: string }> }) { @@ -36,11 +34,19 @@ export default async function GuidesIndexPage({ params }: { params: Promise<{ lo

{labels.guidesTitle}

+

+ {uiCopy.guidesIntro} +

+
+

+ {uiCopy.guidesHelper} +

+
{guides.map((guide, index) => { @@ -53,7 +59,7 @@ export default async function GuidesIndexPage({ params }: { params: Promise<{ lo
{guide.title} = { ua: 'uk' }; -const ogLocaleMap: Record = { - en: 'en_US', - ua: 'uk_UA', - es: 'es_ES', - fr: 'fr_FR', - de: 'de_DE', - it: 'it_IT', - pt: 'pt_PT', - ru: 'ru_RU', - zh: 'zh_CN', - ja: 'ja_JP', - pl: 'pl_PL', -}; -const BRAND = 'generatepasswordto.me'; +const BRAND = BRAND_NAME; const META_TITLE_MAX = 60; const META_DESCRIPTION_MAX = 155; const TITLE_SUFFIX = ` | ${BRAND}`; @@ -102,15 +89,16 @@ const buildMetaDescription = (rawDescription?: string) => { export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise { const { locale } = await params; - const canonical = locale === 'en' ? baseUrl : `${baseUrl}/${locale}`.replace(/\/$/, ''); + const normalizedLocale = normalizeLocale(locale); + const canonical = getLocaleBaseUrl(normalizedLocale).replace(/\/$/, ''); - const seoInfo = seoData.find((data) => data.language === locale) || seoData.find((data) => data.language === 'en'); + const seoInfo = seoData.find((data) => data.language === normalizedLocale) || seoData.find((data) => data.language === 'en'); const metaTitle = buildMetaTitle(seoInfo?.title); const metaDescription = buildMetaDescription(seoInfo?.description); const keywords = seoInfo?.keywords || ['password generator', 'strong password', 'secure password', 'random password', 'password strength']; return { - metadataBase: new URL(baseUrl), + metadataBase: new URL(BASE_URL), title: metaTitle, description: metaDescription, keywords, @@ -130,12 +118,7 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s }, alternates: { canonical, - languages: { - ...Object.fromEntries( - supportedLangs.map((lang) => [hreflangMap[lang] || lang, lang === 'en' ? baseUrl : `${baseUrl}/${lang}`]) - ), - 'x-default': baseUrl, - }, + languages: buildLocaleAlternates(), }, openGraph: { title: metaTitle, @@ -151,7 +134,6 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s alt: 'Generate Password To Me Preview', }, ], - locale: ogLocaleMap[locale] || 'en_US', }, twitter: { card: 'summary_large_image', @@ -177,7 +159,7 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s } export function generateStaticParams() { - return supportedLangs.map((locale) => ({ locale })); + return SUPPORTED_LOCALES.map((locale) => ({ locale })); } export const viewport: Viewport = { @@ -193,44 +175,7 @@ export default async function LocaleLayout({ params: Promise<{ locale: string }>; }) { const { locale } = await params; - - // Generate FAQ Schema data - const stripHtml = (s: string) => s.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim(); - - const t = (key: string): string => { - // @ts-expect-error - dynamic access to lang object - const langEntry = lang[key]; - if (!langEntry || typeof langEntry !== 'object') return ''; - const raw = langEntry[locale]; - return stripHtml(raw || ''); - }; - - const qa = [ - { - q: t('howtouse_title') || 'How to use the password generator?', - a: [t('howtouse_intro'), t('howtouse_step1'), t('howtouse_step2'), t('howtouse_step3')] - .filter(Boolean) - .join(' '), - }, - { - q: t('seotext_why_title') || 'Why use this password generator?', - a: [t('seotext_why1'), t('seotext_why2')].filter(Boolean).join(' '), - }, - { - q: t('security_standards_title') || 'Security standards compliance', - a: [t('nist_compliance_desc'), t('pci_dss_compliance_desc')].filter(Boolean).join(' '), - }, - ].filter(item => item.q && item.a); - - const faqJson = { - '@context': 'https://schema.org', - '@type': 'FAQPage', - mainEntity: qa.map(({ q, a }) => ({ - '@type': 'Question', - name: q, - acceptedAnswer: { '@type': 'Answer', text: a }, - })), - }; + const normalizedLocale = normalizeLocale(locale); return ( <> @@ -278,12 +223,9 @@ export default async function LocaleLayout({ suppressHydrationWarning className={`w-full ${arsenal.variable} ${varelaRound.variable} ${notoSans.variable} ${notoSansJP.variable} ${notoSansSC.variable}`} > -