diff --git a/.gitignore b/.gitignore
index 092fdf49..ac4462cf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,4 @@ dist/
# pnpm
.pnpm-store/
+test-results/
diff --git a/locales/de.json b/locales/de.json
index 8c0817c7..e7bf1a18 100644
--- a/locales/de.json
+++ b/locales/de.json
@@ -140,6 +140,9 @@
"contactUs": "Kontaktieren Sie uns",
"offeringsTitle": "Unsere Support & Care Angebote",
"learnMore": "Mehr erfahren →",
+ "altTexts": {
+ "underline": "Unterstreichung"
+ },
"maven": {
"title": "Support & Care für Apache Maven™",
"description": "Support & Care für Apache Maven™ stärkt die Zukunft des Java-Ökosystems durch nachhaltige Finanzierung und transparente Entwicklung. Sichern Sie die langfristige Stabilität und Weiterentwicklung von Apache Maven."
diff --git a/locales/en.json b/locales/en.json
index 633cafa4..f2a9ad8f 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -140,6 +140,9 @@
"contactUs": "Contact us",
"offeringsTitle": "Our Support & Care Offerings",
"learnMore": "Learn more →",
+ "altTexts": {
+ "underline": "Underline"
+ },
"maven": {
"title": "Support & Care for Apache Maven™",
"description": "Support & Care for Apache Maven™ strengthens the future of the Java ecosystem through sustainable funding and transparent development. Secure the long-term stability and continued development of Apache Maven."
diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx
index 18e4e58b..2b626fba 100644
--- a/src/app/[locale]/layout.tsx
+++ b/src/app/[locale]/layout.tsx
@@ -74,11 +74,11 @@ export default async function LocaleLayout({
const messages = await getMessages();
return (
-
+
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index b8d5fc32..da969b84 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,15 +1,5 @@
import type { Metadata } from 'next'
-import { Montserrat } from 'next/font/google'
import './globals.css'
-import Navbar from '@/components/Navbar'
-import Footer from '@/components/Footer'
-
-const montserrat = Montserrat({
- subsets: ['latin'],
- weight: ['300', '400', '500', '600', '700', '800', '900'],
- variable: '--font-montserrat',
- display: 'swap',
-})
export const metadata: Metadata = {
title: 'Open Elements - Open Source made right',
@@ -33,8 +23,8 @@ export const metadata: Metadata = {
},
}
-// RootLayout is now in [locale]/layout.tsx for i18n support
-// This is just a redirect wrapper
+// Root layout delegates to [locale]/layout.tsx for i18n support
+// The locale layout provides html/body with proper lang attributes
export default function RootLayout({
children,
}: {
diff --git a/src/types/css.d.ts b/src/types/css.d.ts
new file mode 100644
index 00000000..3255cfe2
--- /dev/null
+++ b/src/types/css.d.ts
@@ -0,0 +1 @@
+declare module '*.css';
\ No newline at end of file
diff --git a/tests/e2e/about.spec.ts b/tests/e2e/about.spec.ts
new file mode 100644
index 00000000..c240a444
--- /dev/null
+++ b/tests/e2e/about.spec.ts
@@ -0,0 +1,44 @@
+import { test, expect } from '@playwright/test';
+
+const locales = ['en', 'de'] as const;
+
+function localePath(locale: (typeof locales)[number], path: string = '') {
+ return locale === 'en' ? `/${path}` : `/${locale}/${path}`;
+}
+
+test.describe('About Page', () => {
+ for (const locale of locales) {
+ test(`loads about page correctly for ${locale}`, async ({ page }) => {
+ await page.goto(localePath(locale, 'about'));
+
+ // Page should load successfully
+ await expect(page).toHaveURL(/\/about/);
+
+ // Should have main content
+ await expect(page.locator('main')).toBeVisible();
+
+ // Should maintain locale in URL
+ if (locale === 'de') {
+ await expect(page).toHaveURL(/\/de\/about/);
+ } else {
+ await expect(page).toHaveURL(/^(?!.*\/de\/).*\/about/);
+ }
+ });
+
+ test(`about page navigation works for ${locale}`, async ({ page }) => {
+ await page.goto(localePath(locale));
+
+ // Find and click about link in navigation
+ const aboutLink = page.locator('nav a[href*="about"]').first();
+ await aboutLink.click();
+
+ // Should navigate to about page
+ await expect(page).toHaveURL(/\/about/);
+
+ // Should maintain locale
+ if (locale === 'de') {
+ await expect(page).toHaveURL(/\/de\/about/);
+ }
+ });
+ }
+});
diff --git a/tests/e2e/accessibility.spec.ts b/tests/e2e/accessibility.spec.ts
new file mode 100644
index 00000000..6a62be5c
--- /dev/null
+++ b/tests/e2e/accessibility.spec.ts
@@ -0,0 +1,80 @@
+import { test, expect } from '@playwright/test';
+
+const locales = ['en', 'de'] as const;
+
+function localePath(locale: (typeof locales)[number], path: string = '') {
+ return locale === 'en' ? `/${path}` : `/${locale}/${path}`;
+}
+
+test.describe('Accessibility', () => {
+ for (const locale of locales) {
+ test(`home page has proper heading hierarchy for ${locale}`, async ({ page }) => {
+ await page.goto(localePath(locale));
+
+ // Should have exactly one h1
+ const h1Count = await page.locator('h1').count();
+ expect(h1Count).toBe(1);
+
+ // H1 should have text content
+ const h1Text = await page.locator('h1').first().textContent();
+ expect(h1Text?.trim().length).toBeGreaterThan(0);
+ });
+
+ test(`navigation has proper ARIA landmarks for ${locale}`, async ({ page }) => {
+ await page.goto(localePath(locale));
+
+ // Should have navigation landmark
+ const nav = page.locator('nav, [role="navigation"]');
+ await expect(nav).toBeVisible();
+
+ // Should have main landmark
+ const main = page.locator('main, [role="main"]');
+ await expect(main).toBeVisible();
+ });
+
+ test(`all images have alt text for ${locale}`, async ({ page }) => {
+ await page.goto(localePath(locale));
+
+ // Get all images
+ const images = page.locator('img');
+ const count = await images.count();
+
+ // Check each image has alt attribute
+ for (let i = 0; i < count; i++) {
+ const img = images.nth(i);
+ const alt = await img.getAttribute('alt');
+ // Alt can be empty string for decorative images, but must exist
+ expect(alt).not.toBeNull();
+ }
+ });
+
+ test(`interactive elements are keyboard accessible for ${locale}`, async ({ page }) => {
+ await page.goto(localePath(locale));
+
+ // All links should be keyboard accessible
+ const links = page.locator('a[href]');
+ const linkCount = await links.count();
+
+ if (linkCount > 0) {
+ const firstLink = links.first();
+ await expect(firstLink).toBeVisible();
+
+ // Link should be focusable
+ await firstLink.focus();
+ await expect(firstLink).toBeFocused();
+ }
+ });
+
+ test(`page has valid lang attribute for ${locale}`, async ({ page }) => {
+ await page.goto(localePath(locale));
+
+ const htmlLang = await page.locator('html').getAttribute('lang');
+
+ if (locale === 'de') {
+ expect(htmlLang).toContain('de');
+ } else {
+ expect(htmlLang).toContain('en');
+ }
+ });
+ }
+});
diff --git a/tests/e2e/contact.spec.ts b/tests/e2e/contact.spec.ts
new file mode 100644
index 00000000..2092e488
--- /dev/null
+++ b/tests/e2e/contact.spec.ts
@@ -0,0 +1,47 @@
+import { test, expect } from '@playwright/test';
+
+const locales = ['en', 'de'] as const;
+
+function localePath(locale: (typeof locales)[number], path: string = '') {
+ return locale === 'en' ? `/${path}` : `/${locale}/${path}`;
+}
+
+test.describe('Contact Page', () => {
+ for (const locale of locales) {
+ test(`loads contact page correctly for ${locale}`, async ({ page }) => {
+ await page.goto(localePath(locale, 'contact'));
+
+ // Page should load successfully
+ await expect(page).toHaveURL(/\/contact/);
+
+ // Should have main content
+ await expect(page.locator('main')).toBeVisible();
+
+ // Should maintain locale in URL
+ if (locale === 'de') {
+ await expect(page).toHaveURL(/\/de\/contact/);
+ } else {
+ await expect(page).toHaveURL(/^(?!.*\/de\/).*\/contact/);
+ }
+ });
+
+ test(`contact page navigation works for ${locale}`, async ({ page }) => {
+ await page.goto(localePath(locale));
+
+ // Find and click contact link in navigation
+ const contactLink = page.locator('nav a[href*="contact"]').first();
+
+ if (await contactLink.count() > 0) {
+ await contactLink.click();
+
+ // Should navigate to contact page
+ await expect(page).toHaveURL(/\/contact/);
+
+ // Should maintain locale
+ if (locale === 'de') {
+ await expect(page).toHaveURL(/\/de\/contact/);
+ }
+ }
+ });
+ }
+});
diff --git a/tests/e2e/home.spec.ts b/tests/e2e/home.spec.ts
new file mode 100644
index 00000000..9ec576a2
--- /dev/null
+++ b/tests/e2e/home.spec.ts
@@ -0,0 +1,51 @@
+import { test, expect } from '@playwright/test';
+
+const locales = ['en', 'de'] as const;
+
+function localePath(locale: (typeof locales)[number]) {
+ return locale === 'en' ? '/' : `/${locale}/`;
+}
+
+test.describe('Home Page', () => {
+ for (const locale of locales) {
+ test(`loads home page correctly for ${locale}`, async ({ page }) => {
+ await page.goto(localePath(locale));
+
+ // Page should load successfully
+ await expect(page).toHaveTitle(/Open Elements/i);
+
+ // Should have main navigation
+ await expect(page.locator('nav')).toBeVisible();
+
+ // Should have main content
+ await expect(page.locator('main')).toBeVisible();
+ });
+
+ test(`home page has proper meta tags for ${locale}`, async ({ page }) => {
+ await page.goto(localePath(locale));
+
+ // Should have meta description
+ const metaDescription = page.locator('meta[name="description"]');
+ await expect(metaDescription).toHaveAttribute('content', /.+/);
+
+ // Should have viewport meta tag
+ const viewport = page.locator('meta[name="viewport"]');
+ await expect(viewport).toHaveAttribute('content', /.+/);
+ });
+
+ test(`home page is responsive for ${locale}`, async ({ page }) => {
+ // Test mobile view
+ await page.setViewportSize({ width: 375, height: 667 });
+ await page.goto(localePath(locale));
+ await expect(page.locator('nav')).toBeVisible();
+
+ // Test tablet view
+ await page.setViewportSize({ width: 768, height: 1024 });
+ await expect(page.locator('nav')).toBeVisible();
+
+ // Test desktop view
+ await page.setViewportSize({ width: 1920, height: 1080 });
+ await expect(page.locator('nav')).toBeVisible();
+ });
+ }
+});
diff --git a/tests/e2e/locale-switching.spec.ts b/tests/e2e/locale-switching.spec.ts
new file mode 100644
index 00000000..2cebed8e
--- /dev/null
+++ b/tests/e2e/locale-switching.spec.ts
@@ -0,0 +1,57 @@
+import { test, expect } from '@playwright/test';
+
+test.describe('Locale Switching', () => {
+ test('can switch between EN and DE on home page', async ({ page }) => {
+ // Start with English
+ await page.goto('/');
+ await expect(page).not.toHaveURL(/\/de\//);
+
+ // Find and click language switcher to German
+ const languageSwitcher = page.locator('[data-locale-switcher], a[href*="/de/"], button[aria-label*="Deutsch"], button[aria-label*="German"]').first();
+
+ if (await languageSwitcher.count() > 0) {
+ await languageSwitcher.click();
+
+ // Should switch to German
+ await expect(page).toHaveURL(/\/de\//);
+ }
+ });
+
+ test('maintains page context when switching locale', async ({ page }) => {
+ // Go to about page in English
+ await page.goto('/about');
+
+ // Find language switcher
+ const languageSwitcher = page.locator('[data-locale-switcher], a[href*="/de/about"], button[aria-label*="Deutsch"]').first();
+
+ if (await languageSwitcher.count() > 0) {
+ await languageSwitcher.click();
+
+ // Should be on about page in German
+ await expect(page).toHaveURL(/\/de\/about/);
+ }
+ });
+
+ test('locale persists across navigation', async ({ page }) => {
+ // Start with German
+ await page.goto('/de/');
+
+ // Navigate to about page
+ const aboutLink = page.locator('nav a[href*="about"]').first();
+ if (await aboutLink.count() > 0) {
+ await aboutLink.click();
+
+ // Should still be in German
+ await expect(page).toHaveURL(/\/de\/about/);
+ }
+
+ // Navigate to contact page
+ const contactLink = page.locator('nav a[href*="contact"]').first();
+ if (await contactLink.count() > 0) {
+ await contactLink.click();
+
+ // Should still be in German
+ await expect(page).toHaveURL(/\/de\/contact/);
+ }
+ });
+});
diff --git a/tests/e2e/performance.spec.ts b/tests/e2e/performance.spec.ts
new file mode 100644
index 00000000..3383d6e4
--- /dev/null
+++ b/tests/e2e/performance.spec.ts
@@ -0,0 +1,50 @@
+import { test, expect } from '@playwright/test';
+
+const locales = ['en', 'de'] as const;
+
+function localePath(locale: (typeof locales)[number]) {
+ return locale === 'en' ? '/' : `/${locale}/`;
+}
+
+test.describe('Performance', () => {
+ for (const locale of locales) {
+ test(`home page loads within reasonable time for ${locale}`, async ({ page }) => {
+ const startTime = Date.now();
+
+ await page.goto(localePath(locale), { waitUntil: 'domcontentloaded' });
+
+ const loadTime = Date.now() - startTime;
+
+ // Page should load within 5 seconds
+ expect(loadTime).toBeLessThan(5000);
+ });
+
+ test(`navigation has no console errors for ${locale}`, async ({ page }) => {
+ const errors: string[] = [];
+
+ page.on('console', (msg) => {
+ if (msg.type() === 'error') {
+ errors.push(msg.text());
+ }
+ });
+
+ await page.goto(localePath(locale));
+
+ // Should not have critical console errors
+ expect(errors.length).toBe(0);
+ });
+
+ test(`page has no broken resources for ${locale}`, async ({ page }) => {
+ const failedRequests: string[] = [];
+
+ page.on('requestfailed', (request) => {
+ failedRequests.push(request.url());
+ });
+
+ await page.goto(localePath(locale), { waitUntil: 'networkidle' });
+
+ // Should not have failed resource loads
+ expect(failedRequests.length).toBe(0);
+ });
+ }
+});
diff --git a/tests/e2e/posts.spec.ts b/tests/e2e/posts.spec.ts
new file mode 100644
index 00000000..307f225a
--- /dev/null
+++ b/tests/e2e/posts.spec.ts
@@ -0,0 +1,72 @@
+import { test, expect } from '@playwright/test';
+
+const locales = ['en', 'de'] as const;
+
+function localePath(locale: (typeof locales)[number], path: string = '') {
+ return locale === 'en' ? `/${path}` : `/${locale}/${path}`;
+}
+
+test.describe('Posts Page', () => {
+ for (const locale of locales) {
+ test(`loads posts listing page correctly for ${locale}`, async ({ page }) => {
+ await page.goto(localePath(locale, 'posts'));
+
+ // Page should load successfully
+ await expect(page).toHaveURL(/\/posts/);
+
+ // Should have main content
+ await expect(page.locator('main')).toBeVisible();
+
+ // Should maintain locale in URL
+ if (locale === 'de') {
+ await expect(page).toHaveURL(/\/de\/posts/);
+ } else {
+ await expect(page).toHaveURL(/^(?!.*\/de\/).*\/posts/);
+ }
+ });
+
+ test(`posts page shows list of posts for ${locale}`, async ({ page }) => {
+ await page.goto(localePath(locale, 'posts'));
+
+ // Should have at least one post link
+ const postLinks = page.locator('a[href*="/posts/"]');
+ const count = await postLinks.count();
+ expect(count).toBeGreaterThan(0);
+ });
+
+ test(`can navigate to individual post from listing for ${locale}`, async ({ page }) => {
+ await page.goto(localePath(locale, 'posts'));
+
+ // Get first post link that goes to a specific post (not just /posts)
+ const postLinks = page.locator('a[href*="/posts/"]');
+ const count = await postLinks.count();
+
+ let foundIndividualPost = false;
+ for (let i = 0; i < count; i++) {
+ const link = postLinks.nth(i);
+ const href = await link.getAttribute('href');
+
+ // Check if it's a link to an individual post (contains /posts/slug)
+ if (href && !href.startsWith('http') && href.match(/\/posts\/[^\/]+$/)) {
+ await link.click();
+
+ // Should navigate to individual post
+ await expect(page).toHaveURL(/\/posts\/[^\/]+$/);
+
+ // Should maintain locale
+ if (locale === 'de') {
+ await expect(page).toHaveURL(/\/de\/posts\//);
+ }
+
+ foundIndividualPost = true;
+ break;
+ }
+ }
+
+ // If no individual posts found, at least verify we're on the posts page
+ if (!foundIndividualPost) {
+ await expect(page).toHaveURL(/\/posts/);
+ }
+ });
+ }
+});
diff --git a/tests/e2e/seo.spec.ts b/tests/e2e/seo.spec.ts
new file mode 100644
index 00000000..33752acf
--- /dev/null
+++ b/tests/e2e/seo.spec.ts
@@ -0,0 +1,76 @@
+import { test, expect } from '@playwright/test';
+
+const locales = ['en', 'de'] as const;
+
+function localePath(locale: (typeof locales)[number], path: string = '') {
+ return locale === 'en' ? `/${path}` : `/${locale}/${path}`;
+}
+
+test.describe('SEO', () => {
+ const pages = ['', 'about', 'contact', 'posts', 'support-care'];
+
+ for (const locale of locales) {
+ for (const pagePath of pages) {
+ test(`${pagePath || 'home'} page has title tag for ${locale}`, async ({ page }) => {
+ await page.goto(localePath(locale, pagePath));
+
+ const title = await page.title();
+ expect(title.length).toBeGreaterThan(0);
+ expect(title.length).toBeLessThan(70); // SEO best practice
+ });
+
+ test(`${pagePath || 'home'} page has meta description for ${locale}`, async ({ page }) => {
+ await page.goto(localePath(locale, pagePath));
+
+ const metaDescription = await page.locator('meta[name="description"]').getAttribute('content');
+
+ if (metaDescription) {
+ expect(metaDescription.length).toBeGreaterThan(50);
+ expect(metaDescription.length).toBeLessThan(200); // Allow slightly longer descriptions
+ }
+ });
+
+ test(`${pagePath || 'home'} page has Open Graph tags for ${locale}`, async ({ page }) => {
+ await page.goto(localePath(locale, pagePath));
+
+ // Should have og:title
+ const ogTitle = page.locator('meta[property="og:title"]');
+ await expect(ogTitle).toHaveAttribute('content', /.+/);
+
+ // Should have og:type
+ const ogType = page.locator('meta[property="og:type"]');
+ if (await ogType.count() > 0) {
+ await expect(ogType).toHaveAttribute('content', /.+/);
+ }
+ });
+
+ test(`${pagePath || 'home'} page has canonical URL for ${locale}`, async ({ page }) => {
+ await page.goto(localePath(locale, pagePath));
+
+ const canonical = page.locator('link[rel="canonical"]');
+
+ if (await canonical.count() > 0) {
+ const href = await canonical.getAttribute('href');
+ expect(href).toBeTruthy();
+ }
+ });
+ }
+ }
+
+ test('robots.txt exists and is accessible', async ({ page, baseURL }) => {
+ const response = await page.goto(`${baseURL}/robots.txt`);
+ expect(response?.status()).toBe(200);
+
+ const content = await page.content();
+ expect(content).toContain('User-agent');
+ });
+
+ test('sitemap.xml exists and is accessible', async ({ page, baseURL }) => {
+ const response = await page.goto(`${baseURL}/sitemap.xml`);
+ expect(response?.status()).toBe(200);
+
+ const content = await page.content();
+ // Check for sitemap content (may be wrapped in HTML by browser)
+ expect(content).toMatch(/sitemap|urlset/);
+ });
+});
diff --git a/tests/e2e/support-care.spec.ts b/tests/e2e/support-care.spec.ts
new file mode 100644
index 00000000..2424a6b8
--- /dev/null
+++ b/tests/e2e/support-care.spec.ts
@@ -0,0 +1,45 @@
+import { test, expect } from '@playwright/test';
+
+const locales = ['en', 'de'] as const;
+
+function localePath(locale: (typeof locales)[number], path: string = '') {
+ return locale === 'en' ? `/${path}` : `/${locale}/${path}`;
+}
+
+test.describe('Support Care Pages', () => {
+ for (const locale of locales) {
+ test(`loads support care page correctly for ${locale}`, async ({ page }) => {
+ await page.goto(localePath(locale, 'support-care'));
+
+ // Page should load successfully
+ await expect(page).toHaveURL(/\/support-care/);
+
+ // Should have main content
+ await expect(page.locator('main')).toBeVisible();
+
+ // Should maintain locale in URL
+ if (locale === 'de') {
+ await expect(page).toHaveURL(/\/de\/support-care/);
+ } else {
+ await expect(page).toHaveURL(/^(?!.*\/de\/).*\/support-care/);
+ }
+ });
+
+ test(`loads support care maven page correctly for ${locale}`, async ({ page }) => {
+ await page.goto(localePath(locale, 'support-care-maven'));
+
+ // Page should load successfully
+ await expect(page).toHaveURL(/\/support-care-maven/);
+
+ // Should have main content
+ await expect(page.locator('main')).toBeVisible();
+
+ // Should maintain locale in URL
+ if (locale === 'de') {
+ await expect(page).toHaveURL(/\/de\/support-care-maven/);
+ } else {
+ await expect(page).toHaveURL(/^(?!.*\/de\/).*\/support-care-maven/);
+ }
+ });
+ }
+});