diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100644 index 05fde6f9..00000000 --- a/.husky/pre-commit +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh - -set -e # die on error - -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 -fi \ No newline at end of file diff --git a/package.json b/package.json index ad31b708..e3b7b982 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,7 @@ "build": "next build", "start": "next start", "lint": "eslint .", - "test:e2e": "playwright test", - "prepare": "husky" + "test:e2e": "playwright test" }, "devDependencies": { "@playwright/test": "^1.55.0", @@ -26,7 +25,6 @@ "eslint": "^9.39.2", "eslint-config-next": "^16.1.0", "eslint-config-prettier": "^10.1.8", - "husky": "^9.1.7", "npm-run-all": "^4.1.5", "postcss": "^8.5.6", "prettier": "^3.8.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 64b31175..a1f98651 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -90,9 +90,6 @@ importers: eslint-config-prettier: specifier: ^10.1.8 version: 10.1.8(eslint@9.39.2(jiti@2.6.1)) - husky: - specifier: ^9.1.7 - version: 9.1.7 npm-run-all: specifier: ^4.1.5 version: 4.1.5 @@ -1802,11 +1799,6 @@ packages: html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} - husky@9.1.7: - resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} - engines: {node: '>=18'} - hasBin: true - ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -4597,8 +4589,6 @@ snapshots: html-void-elements@3.0.0: {} - husky@9.1.7: {} - ignore@5.3.2: {} ignore@7.0.5: {} diff --git a/src/i18n/routing.ts b/src/i18n/routing.ts index 3c0ee249..072e1f7a 100644 --- a/src/i18n/routing.ts +++ b/src/i18n/routing.ts @@ -7,9 +7,12 @@ export const routing = defineRouting({ // Used when no locale matches defaultLocale: 'en', - + // The prefix for the default locale when it's in the path - localePrefix: 'as-needed' + localePrefix: 'as-needed', + + // Keep existing unprefixed URLs stable instead of redirecting based on browser language. + localeDetection: false, }); // Lightweight wrappers around Next.js' navigation APIs diff --git a/tests/e2e/code-highlighting.spec.ts b/tests/e2e/code-highlighting.spec.ts index 19191ee1..0e5dc56c 100644 --- a/tests/e2e/code-highlighting.spec.ts +++ b/tests/e2e/code-highlighting.spec.ts @@ -2,16 +2,19 @@ import { test, expect } from '@playwright/test'; test.describe('Code highlighting', () => { test('render production-like syntax colors for fenced code blocks', async ({ page }) => { - await page.goto('/posts/2026-03-12-java-modules-encapsulation-internal-packages'); + await page.goto('/posts/2023-04-18-detect-null-errors-with-static-analysis', { + waitUntil: 'domcontentloaded', + }); + await expect(page.locator('main')).toBeVisible(); const articleBody = page.locator('main .prose').first(); const codeBlock = articleBody.locator('.highlight pre').first(); const keywordToken = codeBlock.locator('.token.keyword').first(); - const punctuationToken = codeBlock.locator('.token.punctuation').first(); + const operatorToken = codeBlock.locator('.token.operator').first(); await expect(codeBlock).toBeVisible(); await expect(codeBlock).toHaveCSS('background-color', 'rgb(39, 40, 34)'); await expect(keywordToken).toHaveCSS('color', 'rgb(102, 217, 239)'); - await expect(punctuationToken).toHaveCSS('color', 'rgb(249, 38, 114)'); + await expect(operatorToken).toHaveCSS('color', 'rgb(249, 38, 114)'); }); }); diff --git a/tests/e2e/markdown-shortcode-links.spec.ts b/tests/e2e/markdown-shortcode-links.spec.ts index 8be731ad..c0c3c254 100644 --- a/tests/e2e/markdown-shortcode-links.spec.ts +++ b/tests/e2e/markdown-shortcode-links.spec.ts @@ -1,32 +1,40 @@ import { test, expect } from '@playwright/test'; test.describe('Markdown shortcode links', () => { - test('render relref links inside markdown link syntax', async ({ page }) => { - await page.goto('/posts/2026-03-12-java-modules-encapsulation-internal-packages'); + test('render relref and ref links to post and top-level routes', async ({ page }) => { + await page.goto('/posts/2026-02-10-review-2025', { + waitUntil: 'domcontentloaded', + }); + await expect(page.locator('main')).toBeVisible(); const articleBody = page.locator('main .prose').first(); const firstArticleLink = articleBody.locator( - 'a[href="/posts/2026-01-27-java-modules-maven4-basics"]', + 'a[href="/posts/2025-01-16-open-elements-in-2024"]', ); - const homeworkLink = articleBody.locator( - 'a[href="/posts/2026-02-26-java-modules-maven4-basics-homework"]', + const supportCareLink = articleBody.locator( + 'a[href="/support-care-maven"]', ); await expect(firstArticleLink).toBeVisible(); - await expect(firstArticleLink).toContainText('first article'); - await expect(homeworkLink).toBeVisible(); - await expect(homeworkLink).toContainText('homework extension'); + await expect(firstArticleLink).toContainText('growth trajectory from 2024'); + await expect(supportCareLink.first()).toBeVisible(); }); - test('preserve relref fragments inside markdown links', async ({ page }) => { - await page.goto('/posts/2026-02-26-java-modules-maven4-basics-homework'); + test('render relref links to nested routes', async ({ page }) => { + await page.goto('/posts/2026-02-10-review-2025', { + waitUntil: 'domcontentloaded', + }); + await expect(page.locator('main')).toBeVisible(); const articleBody = page.locator('main .prose').first(); - const fragmentLink = articleBody.locator( - 'a[href="/posts/2026-01-27-java-modules-maven4-basics#the-module-source-hierarchy"]', + const articleLink = articleBody.locator( + 'a[href="/articles/what-is-maven"]', ); + const employeeLink = articleBody.locator('a[href="/employees/jessie"]'); - await expect(fragmentLink).toBeVisible(); - await expect(fragmentLink).toContainText('module source hierarchy'); + await expect(articleLink).toBeVisible(); + await expect(articleLink).toContainText('work on Apache Maven'); + await expect(employeeLink).toBeVisible(); + await expect(employeeLink).toContainText('Jessy Ssebuliba'); }); }); diff --git a/tests/e2e/markdown-tables.spec.ts b/tests/e2e/markdown-tables.spec.ts index 9ac57965..74b7104d 100644 --- a/tests/e2e/markdown-tables.spec.ts +++ b/tests/e2e/markdown-tables.spec.ts @@ -2,15 +2,18 @@ import { test, expect } from '@playwright/test'; test.describe('Markdown tables', () => { test('render GFM tables in blog posts', async ({ page }) => { - await page.goto('/posts/2026-03-12-java-modules-encapsulation-internal-packages'); + await page.goto('/posts/2020-02-21-adopt-tests', { + waitUntil: 'domcontentloaded', + }); + await expect(page.locator('main')).toBeVisible(); const articleBody = page.locator('main .prose').first(); const table = articleBody.locator('table').first(); await expect(table).toBeVisible(); - await expect(table).toContainText('Aspect'); - await expect(table).toContainText('Classpath'); - await expect(table).toContainText('Module Path'); - await expect(table.locator('tr')).toHaveCount(5); + await expect(table).toContainText('name'); + await expect(table).toContainText('openjdk'); + await expect(table).toContainText('functional'); + await expect(table.locator('tr')).toHaveCount(7); }); }); diff --git a/tests/e2e/post-heading-anchors.spec.ts b/tests/e2e/post-heading-anchors.spec.ts index 4c8b4bae..770c9323 100644 --- a/tests/e2e/post-heading-anchors.spec.ts +++ b/tests/e2e/post-heading-anchors.spec.ts @@ -2,23 +2,28 @@ import { test, expect } from '@playwright/test'; test.describe('Post heading anchors', () => { test('render Hugo-style anchor links for post headlines', async ({ page }) => { - await page.goto('/posts/2026-03-12-java-modules-encapsulation-internal-packages'); + await page.goto('/posts/2026-03-05-oss-ai-slop', { + waitUntil: 'domcontentloaded', + }); + await expect(page.locator('main')).toBeVisible(); const articleBody = page.locator('main .prose').first(); const punctuatedHeading = articleBody.locator( - 'h2#controlling-visibility-with-module-infojava', + 'h2#case-study-an-unusual-contribution-trend-in-the-context-of-major-projects', + ); + const whyItMattersAnchor = articleBody.locator( + 'h2#why-this-ai-slop-matters a[href="#why-this-ai-slop-matters"]', ); - const sourceCodeAnchor = articleBody.locator('h2#source-code a[href="#source-code"]'); await expect(punctuatedHeading).toBeVisible(); await expect( punctuatedHeading.locator( - 'a[href="#controlling-visibility-with-module-infojava"] [data-icon="mdi-link-variant"]', + 'a[href="#case-study-an-unusual-contribution-trend-in-the-context-of-major-projects"] [data-icon="mdi-link-variant"]', ), ).toHaveCount(1); - await expect(sourceCodeAnchor).toBeVisible(); - await sourceCodeAnchor.click(); - await expect(page).toHaveURL(/#source-code$/); + await expect(whyItMattersAnchor).toBeVisible(); + await whyItMattersAnchor.click(); + await expect(page).toHaveURL(/#why-this-ai-slop-matters$/); }); }); diff --git a/tests/e2e/post-images-centering.spec.ts b/tests/e2e/post-images-centering.spec.ts index 2975b667..213a0f1f 100644 --- a/tests/e2e/post-images-centering.spec.ts +++ b/tests/e2e/post-images-centering.spec.ts @@ -27,16 +27,19 @@ async function expectImageToBeCentered(page: Page, imageSelector: string) { test.describe('Post images', () => { test('center standalone images in blog posts', async ({ page }) => { - await page.goto('/posts/2025-01-16-open-elements-in-2024'); + await page.goto('/posts/2020-02-21-adopt-tests', { + waitUntil: 'domcontentloaded', + }); + await expect(page.locator('main')).toBeVisible(); + await expectImageToBeCentered(page, 'img[alt="ci pipeline"]'); + + await page.goto('/posts/2026-03-12-agentic-wallets', { + waitUntil: 'domcontentloaded', + }); + await expect(page.locator('main')).toBeVisible(); await expectImageToBeCentered( page, - 'img[alt="Open Elements continues to focus on open source and Java"]', + 'img[alt="AI agents are blocked by traditional payment infrastructure that requires human identity verification"]', ); - - await page.goto('/posts/2026-01-27-java-modules-maven4-basics'); - await expectImageToBeCentered(page, 'img[alt="Module dependencies"]'); - - await page.goto('/posts/2026-03-05-oss-ai-slop'); - await expectImageToBeCentered(page, 'img[alt="Symbolic image of open source maintenance"]'); }); }); diff --git a/tests/e2e/post-language-switch.spec.ts b/tests/e2e/post-language-switch.spec.ts index 4a6cc617..88081a0f 100644 --- a/tests/e2e/post-language-switch.spec.ts +++ b/tests/e2e/post-language-switch.spec.ts @@ -1,6 +1,22 @@ import { test, expect } from '@playwright/test'; test.describe('Blog post language switch', () => { + test('keeps direct English post URLs stable for German browser preferences', async ({ browser }) => { + const context = await browser.newContext({ + extraHTTPHeaders: { + 'Accept-Language': 'de-DE,de', + }, + }); + const page = await context.newPage(); + + await page.goto('/posts/2026/03/12/agentic-wallets-when-ai-agents-need-to-pay'); + + await expect(page).toHaveURL(/\/posts\/2026\/03\/12\/agentic-wallets-when-ai-agents-need-to-pay\/?$/); + await expect(page.getByRole('heading', { level: 1 })).toContainText('Agentic Wallets'); + + await context.close(); + }); + test('hides the language switch on an English-only post', async ({ page }) => { await page.goto('/posts/2026-03-12-agentic-wallets');