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
10 changes: 8 additions & 2 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

set -e # die on error

pnpm run lint
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)

if command -v pnpm >/dev/null 2>&1; then
cd "$REPO_ROOT"
pnpm run lint
else
echo "pnpm not found. Skipping lint." >&2
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pre-commit hook now allows commits to proceed when pnpm is not found, only printing a warning. This means linting can be bypassed in environments without pnpm installed. Consider whether this is the desired behavior, or if the hook should fail with exit 1 when pnpm is unavailable to ensure consistent code quality checks across all environments.

Suggested change
echo "pnpm not found. Skipping lint." >&2
echo "Error: pnpm not found. Cannot run lint. Please install pnpm to proceed." >&2
exit 1

Copilot uses AI. Check for mistakes.
fi
95 changes: 35 additions & 60 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,90 +1,65 @@
# Open Elements Website

This repo contains the website of Open Elements.
The website is still work in progress.
In future the website will be available at https://www.open-elements.de and https://www.open-elements.com.
This repository contains the Open Elements website.

Netlify status of English page:
## Architecture (2026)

[![Netlify status of English page](https://api.netlify.com/api/v1/badges/0a7875a4-d4ba-4358-8616-87200dcbe7c5/deploy-status)](https://app.netlify.com/sites/open-elements-en/deploys)
The project is now a Next.js application with App Router, Tailwind CSS, and `next-intl` for i18n. Legacy Hugo content and templates are still kept in the repo for migration and historical content.

Netlify status of German page:
### Runtime layers

[![Netlify status of German page](https://api.netlify.com/api/v1/badges/935f5408-eef5-4889-9cb6-ee55a0990a0f/deploy-status)](https://app.netlify.com/sites/open-elements-de/deploys)
- **Next.js App (primary)**
- App Router pages and layouts in `src/app`.
- UI components in `src/components`.
- Shared utilities in `src/lib`, data in `src/data`, types in `src/types`.
- Styling via Tailwind CSS and `src/app/globals.css`.

- **Internationalization**
- `next-intl` routing and helpers in `src/i18n`.
- Translation messages in `locales`.

## Building the website
- **Legacy Hugo content (migrating)**
- Markdown content in `content`.
- Hugo templates in `src/layouts`.
- Hugo configuration in `config.toml`.
- Built static artifacts live in `public` (do not edit manually).

Since the page is based on Hugo and React we use `npm-run-all` to execute several dev executions in parallel.
Therefore you need to install `npm-run-all` as a dev dependency:
- **Web components**
- Custom elements live in `react-src` and are bundled via `react-src/build.mjs` into `public/js`.

```
npm install --save-dev npm-run-all
```


The project is based on [Hugo](https://gohugo.io/) and you need to [install Hugo](https://gohugo.io/installation/) to build the website.
Once Hugo is installed you can host the website on localhost by executing to following command from the root folder of the repository:

```
hugo serve
```
- **E2E tests**
- Playwright specs in `tests/e2e`.

While the process is running the English (default) version of the website can be reached at http://localhost:1313/ and the German can be reached at http://localhost:1314/.
## Development

## Adding Tailwind CSS
### Requirements

### 1-Install Tailwind CSS
- Node.js 22
- pnpm 10

Install tailwindcss via npm, and create your tailwind.config.js file in the root folder.
### Install dependencies

```
npm install -D tailwindcss
npx tailwindcss init
pnpm install
```

### 2-Configure your template paths

Add the paths to all of your template files in your tailwind.config.js file.
### Run locally

```
content: [
"content/**/*.md", "layouts/**/*.html"
],
pnpm run dev
```

### 3-Add the Tailwind directives to your CSS
Create 'input.css' file in the root folder and add the @tailwind directives for each of Tailwind’s layers to your input CSS file.

```
@tailwind base;
@tailwind components;
@tailwind utilities;
```
The app is available at http://localhost:3000.

### 4-Code snippet for Package.json

Add the following code in 'Package.json'
### Build & start

```
"scripts": {
"dev:css": "npx tailwindcss -i input.css -o assets/css/style.css -w",
"dev:hugo": "hugo server",
"dev": "run-p dev:*",
"build:css": "NODE_ENV=production npx tailwindcss -i input.css -o assets/css/style.css -m",
"build:hugo": "hugo",
"build": "run-s build:*"
},
pnpm run build
pnpm run start
```

### 5-Dev environment
For development run the following command in terminal.
```
npm run dev
```
### E2E tests

### 6-Production
For production ready css, run the following command in terminal.
```
npm run build
pnpm run test:e2e
```
3 changes: 2 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default defineConfig([
"build/**",
"next-env.d.ts",
"content/**",
"src/react-src/**"
"public/**",
"react-src/**"
])
]);
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"lint": "eslint .",
"test:e2e": "playwright test",
"prepare": "husky"
},
Expand Down
2 changes: 1 addition & 1 deletion react-src/maven-prs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export default function MavenPRs({status}: { status?: string }) {
}

fetchPRs();
}, []);
}, [status]);

if (!prs) return <div>Keine Pull Requests gefunden</div>;

Expand Down
3 changes: 2 additions & 1 deletion src/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { notFound } from 'next/navigation';
import { routing } from '@/i18n/routing';
import type { Metadata } from 'next'
import { Montserrat } from 'next/font/google'
import Script from 'next/script'
import '../globals.css'
import Navbar from '@/components/Navbar'
import Footer from '@/components/Footer'
Expand Down Expand Up @@ -85,7 +86,7 @@ export default async function LocaleLayout({
<main>{children}</main>
<Footer locale={locale} />
</div>
<script src="https://code.iconify.design/2/2.2.1/iconify.min.js"></script>
<Script src="https://code.iconify.design/2/2.2.1/iconify.min.js" strategy="afterInteractive" />
</NextIntlClientProvider>
</body>
</html>
Expand Down
18 changes: 6 additions & 12 deletions tests/e2e/about.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,11 @@ 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 {
Expand All @@ -27,15 +24,12 @@ test.describe('About Page', () => {

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/);
}
Expand Down
26 changes: 9 additions & 17 deletions tests/e2e/accessibility.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,56 +10,48 @@ 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

Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A useful explanatory comment was removed here. The previous comment "Alt can be empty string for decorative images, but must exist" provided important context about why the test checks for not.toBeNull() rather than checking for a non-empty string. This distinction between decorative images (empty alt) and content images (descriptive alt) is an important accessibility concept. Consider restoring this comment to maintain the test's documentation.

Suggested change
// Alt can be an empty string for decorative images, but the alt attribute must exist.

Copilot uses AI. Check for mistakes.
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();
}
Expand Down
18 changes: 6 additions & 12 deletions tests/e2e/contact.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,11 @@ 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 {
Expand All @@ -27,17 +24,14 @@ test.describe('Contact Page', () => {

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/);
}
Expand Down
23 changes: 8 additions & 15 deletions tests/e2e/home.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,40 +10,33 @@ 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();
});
Expand Down
Loading