Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion next-sitemap.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ module.exports = {
changefreq: 'weekly',
priority: 0.7,
sitemapSize: 5000,
exclude: ['/server-sitemap.xml'],
exclude: ['/', '/server-sitemap.xml'],
robotsTxtOptions: {
policies: [
{
Expand Down
5 changes: 5 additions & 0 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ const nextConfig = {
},
async redirects() {
return [
{
source: '/en',
destination: '/',
permanent: true,
},
{
source: '/:path*',
has: [
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
14 changes: 9 additions & 5 deletions src/app/[locale]/contact/page.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -39,13 +38,18 @@ export default async function ContactPage({ params }: { params: Promise<{ locale
{ label: copy.contact },
]}
/>
<article className="mt-4 rounded-24 border border-[#E5F6FF] dark:border-[#1f2937] bg-white dark:bg-[#0f172a] p-6 md:p-8">
<article className="mt-4 rounded-24 border border-[#E5F6FF] dark:border-[#374151] bg-white dark:bg-[#171717] p-6 md:p-8">
<h1 className="text-3xl md:text-4xl font-bold text-[#071016] dark:text-[#e0e0e0]">{copy.contactTitle}</h1>
<p className="mt-4 text-[#374151] dark:text-[#c7c7c7] leading-7">{copy.contactIntro}</p>
<div className="mt-6 rounded-16 bg-[#F8FDFF] dark:bg-[#111827] border border-[#E5F6FF] dark:border-[#1f2937] p-4">
<ContactCaptcha locale={locale} email={contactEmail} emailLabel={copy.contactEmailLabel} />
<div className="mt-6 rounded-16 bg-[#F8FDFF] dark:bg-[#111827] border border-[#E5F6FF] dark:border-[#374151] p-4">
<p className="text-[#374151] dark:text-[#c7c7c7]">
<span className="font-semibold text-[#071016] dark:text-[#e0e0e0]">{copy.contactEmailLabel}: </span>
<a href={`mailto:${contactEmail}`} className="underline underline-offset-4">
{contactEmail}
</a>
</p>
<p className="mt-2 text-sm text-[#4b5563] dark:text-[#9ca3af]">{copy.contactResponseLabel}</p>
</div>
<p className="mt-2 text-sm text-[#4b5563] dark:text-[#9ca3af]">{copy.contactResponseLabel}</p>
</article>
</section>
</LocalizedPageShell>
Expand Down
84 changes: 72 additions & 12 deletions src/app/[locale]/guides/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = { ua: 'uk' };
export const runtime = 'nodejs';
export const revalidate = 3600;

Expand All @@ -46,17 +47,16 @@ 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,
description: guide.seoDescription,
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,
Expand Down Expand Up @@ -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 (
<LocalizedPageShell locale={locale}>
<JsonLd id={`guide-schema-${slug}`} data={articleSchema} />
<section className="max-w-4xl mx-auto px-4 pt-10 pb-16">
<Breadcrumbs
items={[
{ label: labels.home, href: `/${locale}` },
{ label: labels.home, href: getLocaleHomePath(locale) },
{ label: labels.guides, href: `/${locale}/guides` },
{ label: guide.title },
]}
Expand All @@ -149,17 +165,42 @@ export default async function GuideArticlePage({
<div className="mt-5 relative w-full overflow-hidden rounded-16 aspect-[40/21] bg-[#E5F6FF] dark:bg-[#111827]">
<Image
src={guide.image}
alt={guide.title}
alt={uiCopy.imageAlt(guide.title)}
fill
sizes="(max-width: 1024px) 100vw, 896px"
priority
fetchPriority="high"
className="object-cover"
/>
</div>
<p className="mt-3 text-[#374151] dark:text-[#c7c7c7]">
{renderGuideTextWithLinks(guide.excerpt, linkTargets, interlinkState, 'excerpt')}
</p>
<div className="mt-5 grid grid-cols-1 lg:grid-cols-[1.4fr_0.6fr] gap-5">
<div>
<div className="rounded-24 border border-[#D9ECFF] dark:border-[#243447] bg-[#F8FCFF] dark:bg-[#10161d] p-4 md:p-5">
<p className="text-xs uppercase tracking-wide text-[#2A4E63] dark:text-[#9bdfff]">
{uiCopy.articleSummaryLabel}
</p>
<p className="mt-2 text-[#374151] dark:text-[#c7c7c7] leading-7">
{renderGuideTextWithLinks(guide.excerpt, linkTargets, interlinkState, 'excerpt')}
</p>
</div>
</div>
<aside className="rounded-24 border border-[#D9ECFF] dark:border-[#243447] bg-[#F8FCFF] dark:bg-[#10161d] p-4 md:p-5 h-fit">
<p className="text-xs uppercase tracking-wide text-[#2A4E63] dark:text-[#9bdfff]">
{uiCopy.contentsLabel}
</p>
<nav className="mt-3 space-y-2">
{articleSections.map((section) => (
<a
key={section.id}
href={`#${section.id}`}
className="block text-sm leading-6 text-[#2A4E63] dark:text-[#d7e4ef] underline-offset-4 hover:underline"
>
{section.heading}
</a>
))}
</nav>
</aside>
</div>

<GuideInterlinkBlock entries={introEntries} locale={locale} placement="intro" />

Expand All @@ -172,7 +213,7 @@ export default async function GuideArticlePage({
<div className="mt-8 space-y-8">
{guide.sections.map((section, index) => (
<Fragment key={`${section.heading}-${index}`}>
<section>
<section id={`section-${index + 1}`}>
<h2 className="text-2xl font-semibold text-[#071016] dark:text-[#e0e0e0]">{section.heading}</h2>
<div className="mt-3 space-y-3">
{section.paragraphs.map((paragraph, pIndex) => (
Expand All @@ -195,6 +236,25 @@ export default async function GuideArticlePage({
</Fragment>
))}
<GuideInterlinkBlock entries={conclusionEntries} locale={locale} placement="conclusion" />
<div className="rounded-24 border border-[#D9ECFF] dark:border-[#243447] bg-[#F8FCFF] dark:bg-[#10161d] p-5 md:p-6">
<p className="text-xs uppercase tracking-wide text-[#2A4E63] dark:text-[#9bdfff]">
{uiCopy.nextStepsLabel}
</p>
<div className="mt-4 flex flex-col sm:flex-row gap-3">
<Link
href={`/${locale}/guides`}
className="inline-flex items-center justify-center rounded-24 bg-[#2A4E63] px-4 py-2 text-sm font-semibold text-white transition hover:bg-[#1f3b4c]"
>
{uiCopy.backToGuidesLabel}
</Link>
<Link
href={`/${locale}/contact`}
className="inline-flex items-center justify-center rounded-24 border border-[#2A4E63] dark:border-[#9bdfff] px-4 py-2 text-sm font-semibold text-[#2A4E63] dark:text-[#9bdfff] transition hover:bg-[#E5F6FF] dark:hover:bg-[#0A1A2B]"
>
{uiCopy.contactLabel}
</Link>
</div>
</div>
</div>

</article>
Expand Down
30 changes: 18 additions & 12 deletions src/app/[locale]/guides/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Metadata> {
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 }> }) {
Expand All @@ -36,11 +34,19 @@ export default async function GuidesIndexPage({ params }: { params: Promise<{ lo
<section className="max-w-6xl mx-auto px-4 py-10">
<Breadcrumbs
items={[
{ label: labels.home, href: `/${locale}` },
{ label: labels.home, href: getLocaleHomePath(locale) },
{ label: labels.guides },
]}
/>
<h1 className="mt-4 text-3xl md:text-4xl font-bold text-[#071016] dark:text-[#e0e0e0]">{labels.guidesTitle}</h1>
<p className="mt-4 max-w-3xl text-[16px] md:text-[18px] leading-7 text-[#425466] dark:text-[#c7c7c7]">
{uiCopy.guidesIntro}
</p>
<div className="mt-5 rounded-24 border border-[#D9ECFF] dark:border-[#243447] bg-[#F8FCFF] dark:bg-[#10161d] p-4 md:p-5">
<p className="text-sm md:text-base leading-6 text-[#2A4E63] dark:text-[#d7e4ef]">
{uiCopy.guidesHelper}
</p>
</div>

<div className="mt-8 grid grid-cols-1 md:grid-cols-2 gap-5">
{guides.map((guide, index) => {
Expand All @@ -53,7 +59,7 @@ export default async function GuidesIndexPage({ params }: { params: Promise<{ lo
<div className="relative w-full overflow-hidden aspect-[16/7] bg-[#E5F6FF] dark:bg-[#111827]">
<Image
src={guide.image}
alt={guide.title}
alt={uiCopy.imageAlt(guide.title)}
fill
sizes="(max-width: 768px) 100vw, 50vw"
priority={index < 2}
Expand Down
Loading