From 9f1fb349c2e4ad8749db10b6c0b05d8af8da512a Mon Sep 17 00:00:00 2001 From: Do Siki Date: Sat, 22 Aug 2026 12:12:48 +0200 Subject: [PATCH] =?UTF-8?q?fix(web):=20hygiene=20=E2=80=94=20OG=20image/me?= =?UTF-8?q?tadataBase,=20Header=20a11y=20text=20+=20nav=20flag,=20dead=20c?= =?UTF-8?q?ode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - siteConfig.ogImage pointed to an existing asset; metadataBase added so relative OG/Twitter URLs resolve against the real origin instead of localhost - Header sr-only menu label moved to common.json (a11y.openMenu/closeMenu); navigation primary styling driven by a NavigationItem.primary flag instead of brittle 'Kapcsolat' string comparisons - remove dead code: lib/site-config.ts (unused hybrid config) and the unused, schema-divergent siteConfig.contact.form - ContactConfig type cleaned up (address/form removed) Closes MITHOME-77 --- proto/src/app/layout.tsx | 3 + proto/src/components/Header.test.tsx | 9 +- proto/src/components/Header.tsx | 17 +- proto/src/config/site.ts | 33 +-- proto/src/content/common.json | 4 + proto/src/content/schema.js | 1 + proto/src/content/types.ts | 4 + proto/src/lib/site-config.test.ts | 382 --------------------------- proto/src/lib/site-config.ts | 74 ------ proto/src/types/site.ts | 28 +- 10 files changed, 31 insertions(+), 524 deletions(-) delete mode 100755 proto/src/lib/site-config.test.ts delete mode 100755 proto/src/lib/site-config.ts diff --git a/proto/src/app/layout.tsx b/proto/src/app/layout.tsx index cf49901..fc621b5 100755 --- a/proto/src/app/layout.tsx +++ b/proto/src/app/layout.tsx @@ -19,6 +19,9 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { + // WHY metadataBase: without it Next resolves relative OG/twitter image URLs + // against localhost, producing broken social previews in production. + metadataBase: new URL(siteConfig.general.url), title: `${siteConfig.general.name} | ${siteConfig.general.description}`, description: siteConfig.general.description, authors: [{ name: siteConfig.general.name }], diff --git a/proto/src/components/Header.test.tsx b/proto/src/components/Header.test.tsx index b0d4f72..23867ed 100755 --- a/proto/src/components/Header.test.tsx +++ b/proto/src/components/Header.test.tsx @@ -2,6 +2,7 @@ import { render, screen, fireEvent } from '@testing-library/react' import '@testing-library/jest-dom' import userEvent from '@testing-library/user-event' import Header from './Header' +import { common } from '@/content' // Mock Next.js Link component jest.mock('next/link', () => { @@ -52,14 +53,14 @@ describe('Header', () => { // The hamburger menu button is hidden by default in desktop view // We can test its presence even if not visible - const hamburgerButton = screen.getByRole('button', { name: /open main menu/i }) + const hamburgerButton = screen.getByRole('button', { name: new RegExp(common.a11y.openMenu, 'i') }) expect(hamburgerButton).toBeInTheDocument() }) it('should have proper accessibility attributes', () => { render(
) - const hamburgerButton = screen.getByRole('button', { name: /open main menu/i }) + const hamburgerButton = screen.getByRole('button', { name: new RegExp(common.a11y.openMenu, 'i') }) expect(hamburgerButton).toHaveAttribute('aria-expanded', 'false') }) @@ -82,7 +83,7 @@ describe('Header', () => { const user = userEvent.setup() render(
) - const hamburgerButton = screen.getByRole('button', { name: /open main menu/i }) + const hamburgerButton = screen.getByRole('button', { name: new RegExp(common.a11y.openMenu, 'i') }) // Initially menu should be closed expect(hamburgerButton).toHaveAttribute('aria-expanded', 'false') @@ -100,7 +101,7 @@ describe('Header', () => { const user = userEvent.setup() render(
) - const hamburgerButton = screen.getByRole('button', { name: /open main menu/i }) + const hamburgerButton = screen.getByRole('button', { name: new RegExp(common.a11y.openMenu, 'i') }) // Open the mobile menu await user.click(hamburgerButton) diff --git a/proto/src/components/Header.tsx b/proto/src/components/Header.tsx index 5aa3dfb..1f3dafb 100755 --- a/proto/src/components/Header.tsx +++ b/proto/src/components/Header.tsx @@ -1,6 +1,7 @@ 'use client' import { siteConfig } from '@/config/site' +import { common } from '@/content' import { useState, useEffect } from 'react' import { ThemeToggle } from './ThemeProvider' @@ -66,15 +67,15 @@ export default function Header() { target={item.external ? '_blank' : undefined} rel={item.external ? 'noopener noreferrer' : undefined} className={`relative px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 ${ - item.label === 'Kapcsolat' + item.primary ? 'btn btn-primary text-white' : 'hover-scale' }`} - style={item.label !== 'Kapcsolat' ? { + style={!item.primary ? { color: 'var(--color-foreground)', } : undefined} > - {item.label !== 'Kapcsolat' && ( + {!item.primary && ( - {isMenuOpen ? 'Close main menu' : 'Open main menu'} + {isMenuOpen ? common.a11y.closeMenu : common.a11y.openMenu}
{/* Hamburger to X animation */} setIsMenuOpen(false)} className={`block px-4 py-3 rounded-lg text-base font-medium transition-all duration-200 ${ - item.label === 'Kapcsolat' ? 'btn btn-primary text-white mt-2' : '' + item.primary ? 'btn btn-primary text-white mt-2' : '' }`} style={{ animationDelay: `${index * 50}ms`, - ...(item.label !== 'Kapcsolat' ? { + ...(!item.primary ? { color: 'var(--color-foreground)', background: 'transparent', } : {}) }} onMouseEnter={(e) => { - if (item.label !== 'Kapcsolat') { + if (!item.primary) { e.currentTarget.style.background = 'var(--color-primary-50)' e.currentTarget.style.color = 'var(--color-primary-600)' } }} onMouseLeave={(e) => { - if (item.label !== 'Kapcsolat') { + if (!item.primary) { e.currentTarget.style.background = 'transparent' e.currentTarget.style.color = 'var(--color-foreground)' } diff --git a/proto/src/config/site.ts b/proto/src/config/site.ts index 543be18..c83a022 100755 --- a/proto/src/config/site.ts +++ b/proto/src/config/site.ts @@ -10,7 +10,7 @@ export const siteConfig: SiteConfig = { name: 'mozdIT Bt.', description: 'Megbízható web- és email szolgáltatás személyre szabott támogatással. Stabil tárhely, üzembiztos levelezés és DNS adminisztráció gyors reakcióval.', url: process.env.NEXT_PUBLIC_SITE_URL || 'https://localhost:3000', - ogImage: '/og-image.png', + ogImage: '/mozdit_logo_text.png', locale: 'hu-HU' }, @@ -19,7 +19,7 @@ export const siteConfig: SiteConfig = { { label: 'Kezdőlap', href: '/' }, { label: 'Rólunk', href: '/rolunk' }, { label: 'Szolgáltatások', href: '/szolgaltatasok' }, - { label: 'Kapcsolat', href: '/kapcsolat' } + { label: 'Kapcsolat', href: '/kapcsolat', primary: true } ], footer: [ { label: 'Kezdőlap', href: '/' }, @@ -41,32 +41,7 @@ export const siteConfig: SiteConfig = { contact: { email: process.env.NEXT_PUBLIC_CONTACT_EMAIL || 'info@mozdit.hu', - // address lives in content/common.json (footer.address) — CMS-editable - form: { - title: 'Kapcsolatfelvétel', - description: 'Legyen szíves érdeklődését vagy problémáját részletesen megfogalmazni.', - submitText: 'Üzenet küldése', - fields: { - name: { - label: 'Név', - placeholder: 'Vezetéknév Keresztnév', - required: true - }, - email: { - label: 'Email cím', - placeholder: 'pelda@email.hu', - required: true - }, - message: { - label: 'Üzenet', - placeholder: 'Kérjük írja le érdeklődését részletesen...', - required: true - }, - consent: { - label: 'Elfogadom az adatkezelési tájékoztatót', - required: true - } - } - } + // address lives in content/common.json (footer.address) — CMS-editable. + // The form fields live in content/pages/contact.json. } } \ No newline at end of file diff --git a/proto/src/content/common.json b/proto/src/content/common.json index 275dc23..464562e 100644 --- a/proto/src/content/common.json +++ b/proto/src/content/common.json @@ -20,5 +20,9 @@ "footer": { "copyright": "© 2002–{year} mozdIT Bt. Minden jog fenntartva.", "address": "Szigetszentmiklós, Magyarország" + }, + "a11y": { + "openMenu": "Főmenü megnyitása", + "closeMenu": "Főmenü bezárása" } } diff --git a/proto/src/content/schema.js b/proto/src/content/schema.js index d14df02..e7693e9 100644 --- a/proto/src/content/schema.js +++ b/proto/src/content/schema.js @@ -16,6 +16,7 @@ const schemas = { validation: object({ required: string, invalidEmail: string, minLength: string }), staging: object({ banner: string }), footer: object({ copyright: string, address: string }), + a11y: object({ openMenu: string, closeMenu: string }), }), home: object({ hero: object({ title: string, subtitle: string, description: string, trustBullets: array(string), cta: object({ primary: ctaLink, secondary: ctaLink }) }), diff --git a/proto/src/content/types.ts b/proto/src/content/types.ts index 74335c7..5d44066 100755 --- a/proto/src/content/types.ts +++ b/proto/src/content/types.ts @@ -197,6 +197,10 @@ export interface CommonContent { copyright: string address: string } + a11y: { + openMenu: string + closeMenu: string + } } export interface LegalPageContent { diff --git a/proto/src/lib/site-config.test.ts b/proto/src/lib/site-config.test.ts deleted file mode 100755 index 2afcbb2..0000000 --- a/proto/src/lib/site-config.test.ts +++ /dev/null @@ -1,382 +0,0 @@ -import { getSiteConfig, saveSiteConfig, initializeDefaultConfig } from './site-config' -import { getCollection } from './mongodb' -import { siteConfig as staticConfig } from '@/config/site' -import { SiteConfig } from '@/types/site' -import logger from './logger' - -// Mock dependencies -jest.mock('./mongodb') -jest.mock('./logger', () => ({ - __esModule: true, - default: { - info: jest.fn(), - warn: jest.fn(), - error: jest.fn() - } -})) -jest.mock('@/config/site', () => ({ - siteConfig: { - general: { - name: 'Test Site', - description: 'Test Description', - url: 'https://test.com', - ogImage: 'https://test.com/og.jpg', - locale: 'en' - }, - navigation: { - main: [ - { label: 'Home', href: '/' }, - { label: 'About', href: '/about' } - ], - footer: [ - { label: 'Privacy', href: '/privacy' }, - { label: 'Terms', href: '/terms' } - ] - }, - hero: { - title: 'Welcome', - subtitle: 'Test Subtitle', - description: 'Test description', - cta: { - primary: { - text: 'Get Started', - href: '/get-started' - } - } - }, - services: { - title: 'Our Services', - subtitle: 'What we offer', - services: [] - }, - about: { - title: 'About Us', - description: ['Test about section'], - usps: [] - }, - footer: { - copyright: '© 2024 Test Site', - links: [] - }, - contact: { - email: 'test@test.com', - address: 'Test Address', - form: { - title: 'Contact Us', - description: 'Get in touch', - submitText: 'Send Message', - fields: { - name: { label: 'Name', placeholder: 'Your name', required: true }, - email: { label: 'Email', placeholder: 'your@email.com', required: true }, - message: { label: 'Message', placeholder: 'Your message', required: true }, - consent: { label: 'I agree', required: true } - } - } - } - } -})) - -const mockCollection = { - findOne: jest.fn(), - replaceOne: jest.fn() -} - - - -describe('Site Config', () => { - beforeEach(() => { - jest.clearAllMocks() - ;(getCollection as jest.Mock).mockResolvedValue(mockCollection) - }) - - describe('getSiteConfig', () => { - it('should return MongoDB config when available', async () => { - const mockConfig: SiteConfig = { - general: { - name: 'MongoDB Site', - description: 'MongoDB Description', - url: 'https://mongodb.com', - ogImage: 'https://mongodb.com/og.jpg', - locale: 'en' - }, - navigation: { - main: [{ label: 'Home', href: '/' }], - footer: [{ label: 'Privacy', href: '/privacy' }] - }, - hero: { - title: 'MongoDB Hero', - subtitle: 'MongoDB Subtitle', - description: 'MongoDB Description', - cta: { - primary: { - text: 'Get Started', - href: '/get-started' - } - } - }, - services: { - title: 'Services', - subtitle: 'Our services', - services: [] - }, - about: { - title: 'About', - description: ['About us'], - usps: [] - }, - footer: { - copyright: '© 2024 MongoDB', - links: [] - }, - contact: { - email: 'mongodb@test.com', - address: 'MongoDB Address', - form: { - title: 'Contact', - description: 'Get in touch', - submitText: 'Send', - fields: { - name: { label: 'Name', placeholder: 'Name', required: true }, - email: { label: 'Email', placeholder: 'Email', required: true }, - message: { label: 'Message', placeholder: 'Message', required: true }, - consent: { label: 'Consent', required: true } - } - } - } - } - - mockCollection.findOne.mockResolvedValue({ - _id: 'mock-id', - data: mockConfig - }) - - const config = await getSiteConfig() - - expect(config).toEqual(mockConfig) - expect(mockCollection.findOne).toHaveBeenCalledWith({ - type: 'site_config', - environment: 'development' - }) - expect(logger.info).toHaveBeenCalledWith( - 'Loaded site config from MongoDB', - { configId: 'mock-id' } - ) - }) - - it('should return static config when MongoDB config not found', async () => { - mockCollection.findOne.mockResolvedValue(null) - - const config = await getSiteConfig() - - expect(config).toEqual(staticConfig) - expect(logger.info).toHaveBeenCalledWith( - 'MongoDB config not found, using static fallback' - ) - }) - - it('should return static config when MongoDB is unavailable', async () => { - mockCollection.findOne.mockRejectedValue(new Error('Connection failed')) - - const config = await getSiteConfig() - - expect(config).toEqual(staticConfig) - expect(logger.warn).toHaveBeenCalledWith( - 'MongoDB unavailable, using static config', - { error: 'Connection failed' } - ) - }) - }) - - describe('saveSiteConfig', () => { - it('should save config to MongoDB successfully', async () => { - const newConfig: SiteConfig = { - general: { - name: 'New Site', - description: 'New Description', - url: 'https://new.com', - ogImage: 'https://new.com/og.jpg', - locale: 'en' - }, - navigation: { - main: [{ label: 'Home', href: '/' }], - footer: [{ label: 'Privacy', href: '/privacy' }] - }, - hero: { - title: 'New Hero', - subtitle: 'New Subtitle', - description: 'New Description', - cta: { - primary: { - text: 'Get Started', - href: '/get-started' - } - } - }, - services: { - title: 'Services', - subtitle: 'Our services', - services: [] - }, - about: { - title: 'About', - description: ['About us'], - usps: [] - }, - footer: { - copyright: '© 2024 New', - links: [] - }, - contact: { - email: 'new@test.com', - address: 'New Address', - form: { - title: 'Contact', - description: 'Get in touch', - submitText: 'Send', - fields: { - name: { label: 'Name', placeholder: 'Name', required: true }, - email: { label: 'Email', placeholder: 'Email', required: true }, - message: { label: 'Message', placeholder: 'Message', required: true }, - consent: { label: 'Consent', required: true } - } - } - } - } - - mockCollection.replaceOne.mockResolvedValue({ acknowledged: true }) - - const result = await saveSiteConfig(newConfig) - - expect(result).toBe(true) - expect(mockCollection.replaceOne).toHaveBeenCalledWith( - { type: 'site_config', environment: 'development' }, - { - type: 'site_config', - environment: 'development', - data: newConfig, - lastModified: expect.any(Date) - }, - { upsert: true } - ) - expect(logger.info).toHaveBeenCalledWith('Saved site config to MongoDB') - }) - - it('should return false when save fails', async () => { - const newConfig: SiteConfig = { - general: { - name: 'New Site', - description: 'New Description', - url: 'https://new.com', - ogImage: 'https://new.com/og.jpg', - locale: 'en' - }, - navigation: { - main: [{ label: 'Home', href: '/' }], - footer: [{ label: 'Privacy', href: '/privacy' }] - }, - hero: { - title: 'New Hero', - subtitle: 'New Subtitle', - description: 'New Description', - cta: { - primary: { - text: 'Get Started', - href: '/get-started' - } - } - }, - services: { - title: 'Services', - subtitle: 'Our services', - services: [] - }, - about: { - title: 'About', - description: ['About us'], - usps: [] - }, - footer: { - copyright: '© 2024 New', - links: [] - }, - contact: { - email: 'new@test.com', - address: 'New Address', - form: { - title: 'Contact', - description: 'Get in touch', - submitText: 'Send', - fields: { - name: { label: 'Name', placeholder: 'Name', required: true }, - email: { label: 'Email', placeholder: 'Email', required: true }, - message: { label: 'Message', placeholder: 'Message', required: true }, - consent: { label: 'Consent', required: true } - } - } - } - } - - mockCollection.replaceOne.mockRejectedValue(new Error('Save failed')) - - const result = await saveSiteConfig(newConfig) - - expect(result).toBe(false) - expect(logger.error).toHaveBeenCalledWith( - 'Failed to save config to MongoDB', - { error: 'Save failed' } - ) - }) - }) - - describe('initializeDefaultConfig', () => { - it('should initialize default config when none exists', async () => { - mockCollection.findOne.mockResolvedValue(null) - mockCollection.replaceOne.mockResolvedValue({ acknowledged: true }) - - await initializeDefaultConfig() - - expect(mockCollection.findOne).toHaveBeenCalledWith({ - type: 'site_config', - environment: 'development' - }) - expect(mockCollection.replaceOne).toHaveBeenCalledWith( - { type: 'site_config', environment: 'development' }, - { - type: 'site_config', - environment: 'development', - data: staticConfig, - lastModified: expect.any(Date) - }, - { upsert: true } - ) - expect(logger.info).toHaveBeenCalledWith( - 'Initialized default site config in MongoDB' - ) - }) - - it('should not initialize when config already exists', async () => { - mockCollection.findOne.mockResolvedValue({ - _id: 'existing-id', - data: staticConfig - }) - - await initializeDefaultConfig() - - expect(mockCollection.replaceOne).not.toHaveBeenCalled() - expect(logger.info).toHaveBeenCalledWith( - 'Site config already exists in MongoDB' - ) - }) - - it('should handle initialization errors gracefully', async () => { - mockCollection.findOne.mockRejectedValue(new Error('Connection failed')) - - await initializeDefaultConfig() - - expect(logger.error).toHaveBeenCalledWith( - 'Failed to initialize site config', - { error: 'Connection failed' } - ) - }) - }) -}) \ No newline at end of file diff --git a/proto/src/lib/site-config.ts b/proto/src/lib/site-config.ts deleted file mode 100755 index 8ba5835..0000000 --- a/proto/src/lib/site-config.ts +++ /dev/null @@ -1,74 +0,0 @@ -// Hybrid approach: File-based fallback + MongoDB integration for future - -import { siteConfig as staticConfig } from '@/config/site' -import { SiteConfig } from '@/types/site' -import { getCollection } from './mongodb' -import logger from './logger' - -/** - * Get site configuration with hybrid approach - * 1. Try MongoDB first (production-ready) - * 2. Fall back to static file (development/development safe) - */ -export async function getSiteConfig(): Promise { - try { - const collection = await getCollection('site_config') - const doc = await collection.findOne({ type: 'site_config', environment: 'development' }) - - if (doc && doc.data) { - logger.info('Loaded site config from MongoDB', { configId: doc._id }) - return doc.data as SiteConfig - } else { - logger.info('MongoDB config not found, using static fallback') - return staticConfig - } - } catch (error) { - logger.warn('MongoDB unavailable, using static config', { error: (error as Error).message }) - return staticConfig - } -} - -/** - * Save site configuration to MongoDB - * For future admin panel integration - */ -export async function saveSiteConfig(config: SiteConfig): Promise { - try { - const collection = await getCollection('site_config') - await collection.replaceOne( - { type: 'site_config', environment: 'development' }, - { - type: 'site_config', - environment: 'development', - data: config, - lastModified: new Date() - }, - { upsert: true } - ) - - logger.info('Saved site config to MongoDB') - return true - } catch (error) { - logger.error('Failed to save config to MongoDB', { error: (error as Error).message }) - return false - } -} - -/** - * Initialize default config in MongoDB (one-time setup) - */ -export async function initializeDefaultConfig(): Promise { - try { - const collection = await getCollection('site_config') - const existingConfig = await collection.findOne({ type: 'site_config', environment: 'development' }) - - if (!existingConfig) { - await saveSiteConfig(staticConfig) - logger.info('Initialized default site config in MongoDB') - } else { - logger.info('Site config already exists in MongoDB') - } - } catch (error) { - logger.error('Failed to initialize site config', { error: (error as Error).message }) - } -} \ No newline at end of file diff --git a/proto/src/types/site.ts b/proto/src/types/site.ts index 092517e..ddd3baa 100755 --- a/proto/src/types/site.ts +++ b/proto/src/types/site.ts @@ -20,6 +20,7 @@ export interface NavigationItem { label: string href: string external?: boolean + primary?: boolean } export interface NavigationConfig { @@ -83,38 +84,11 @@ export interface FooterConfig { export interface ContactConfig { phone?: string email: string - address: string socialMedia?: { platform: string url: string label: string }[] - form: { - title: string - description: string - submitText: string - fields: { - name: { - label: string - placeholder: string - required: boolean - } - email: { - label: string - placeholder: string - required: boolean - } - message: { - label: string - placeholder: string - required: boolean - } - consent: { - label: string - required: boolean - } - } - } } export interface EnvConfig {