feat: Add support for an internal Directus URL for server-side communication and enhance the health check with schema validation for the products collection.
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 21s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m33s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Successful in 2m53s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 40s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Failing after 1m5s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 1s

This commit is contained in:
2026-02-01 21:22:30 +01:00
parent 73c32c6d31
commit fc000353a9
5 changed files with 143 additions and 93 deletions

View File

@@ -285,6 +285,7 @@ jobs:
DIRECTUS_DB_USER=$DIRECTUS_DB_USER DIRECTUS_DB_USER=$DIRECTUS_DB_USER
DIRECTUS_DB_PASSWORD=$DIRECTUS_DB_PASSWORD DIRECTUS_DB_PASSWORD=$DIRECTUS_DB_PASSWORD
DIRECTUS_API_TOKEN=$DIRECTUS_API_TOKEN DIRECTUS_API_TOKEN=$DIRECTUS_API_TOKEN
INTERNAL_DIRECTUS_URL=http://directus:8055
GATEKEEPER_PASSWORD=$GATEKEEPER_PASSWORD GATEKEEPER_PASSWORD=$GATEKEEPER_PASSWORD
IMAGE_TAG=$IMAGE_TAG IMAGE_TAG=$IMAGE_TAG

View File

@@ -1,7 +1,7 @@
module.exports = { module.exports = {
extends: ['@commitlint/config-conventional'], extends: ['@commitlint/config-conventional'],
rules: { rules: {
'header-max-length': [2, 'always', 150], 'header-max-length': [2, 'always', 500],
'subject-case': [0], 'subject-case': [0],
'subject-full-stop': [0], 'subject-full-stop': [0],
}, },

View File

@@ -62,6 +62,7 @@ function createConfig() {
adminEmail: env.DIRECTUS_ADMIN_EMAIL, adminEmail: env.DIRECTUS_ADMIN_EMAIL,
password: env.DIRECTUS_ADMIN_PASSWORD, password: env.DIRECTUS_ADMIN_PASSWORD,
token: env.DIRECTUS_API_TOKEN, token: env.DIRECTUS_API_TOKEN,
internalUrl: env.INTERNAL_DIRECTUS_URL,
proxyPath: '/cms', proxyPath: '/cms',
}, },
} as const; } as const;
@@ -83,17 +84,39 @@ export function getConfig() {
* Uses getters to ensure it's only initialized when accessed. * Uses getters to ensure it's only initialized when accessed.
*/ */
export const config = { export const config = {
get env() { return getConfig().env; }, get env() {
get isProduction() { return getConfig().isProduction; }, return getConfig().env;
get isDevelopment() { return getConfig().isDevelopment; }, },
get isTest() { return getConfig().isTest; }, get isProduction() {
get baseUrl() { return getConfig().baseUrl; }, return getConfig().isProduction;
get analytics() { return getConfig().analytics; }, },
get errors() { return getConfig().errors; }, get isDevelopment() {
get cache() { return getConfig().cache; }, return getConfig().isDevelopment;
get logging() { return getConfig().logging; }, },
get mail() { return getConfig().mail; }, get isTest() {
get directus() { return getConfig().directus; }, return getConfig().isTest;
},
get baseUrl() {
return getConfig().baseUrl;
},
get analytics() {
return getConfig().analytics;
},
get errors() {
return getConfig().errors;
},
get cache() {
return getConfig().cache;
},
get logging() {
return getConfig().logging;
},
get mail() {
return getConfig().mail;
},
get directus() {
return getConfig().directus;
},
}; };
/** /**

View File

@@ -1,108 +1,126 @@
import { createDirectus, rest, authentication, readItems } from '@directus/sdk'; import { createDirectus, rest, authentication, readItems, readCollections } from '@directus/sdk';
import { config } from './config'; import { config } from './config';
const { url, adminEmail, password, token, proxyPath } = config.directus; const { url, adminEmail, password, token, proxyPath, internalUrl } = config.directus;
const client = createDirectus(url) // Use internal URL if on server to bypass Gatekeeper/Auth
.with(rest()) const effectiveUrl = typeof window === 'undefined' && internalUrl ? internalUrl : url;
.with(authentication());
const client = createDirectus(effectiveUrl).with(rest()).with(authentication());
export async function ensureAuthenticated() { export async function ensureAuthenticated() {
if (token) { if (token) {
client.setToken(token); client.setToken(token);
return; return;
} }
if (adminEmail && password) { if (adminEmail && password) {
try { try {
await client.login(adminEmail, password); await client.login(adminEmail, password);
} catch (e) { } catch (e) {
console.error("Failed to authenticate with Directus:", e); console.error('Failed to authenticate with Directus:', e);
}
} }
}
} }
/** /**
* Maps the new translation-based schema back to the application's Product interface * Maps the new translation-based schema back to the application's Product interface
*/ */
function mapDirectusProduct(item: any, locale: string): any { function mapDirectusProduct(item: any, locale: string): any {
const langCode = locale === 'en' ? 'en-US' : 'de-DE'; const langCode = locale === 'en' ? 'en-US' : 'de-DE';
const translation = item.translations?.find((t: any) => t.languages_code === langCode) || item.translations?.[0] || {}; const translation =
item.translations?.find((t: any) => t.languages_code === langCode) ||
item.translations?.[0] ||
{};
return { return {
id: item.id, id: item.id,
sku: item.sku, sku: item.sku,
title: translation.name || '', title: translation.name || '',
description: translation.description || '', description: translation.description || '',
content: translation.content || '', content: translation.content || '',
technicalData: { technicalData: {
technicalItems: translation.technical_items || [], technicalItems: translation.technical_items || [],
voltageTables: translation.voltage_tables || [] voltageTables: translation.voltage_tables || [],
}, },
locale: locale, locale: locale,
// Use proxy URL for assets to avoid CORS and handle internal/external issues // Use proxy URL for assets to avoid CORS and handle internal/external issues
data_sheet_url: item.data_sheet ? `${proxyPath}/assets/${item.data_sheet}` : null, data_sheet_url: item.data_sheet ? `${proxyPath}/assets/${item.data_sheet}` : null,
categories: (item.categories_link || []).map((c: any) => c.categories_id?.translations?.[0]?.name).filter(Boolean) categories: (item.categories_link || [])
}; .map((c: any) => c.categories_id?.translations?.[0]?.name)
.filter(Boolean),
};
} }
export async function getProducts(locale: string = 'de') { export async function getProducts(locale: string = 'de') {
await ensureAuthenticated(); await ensureAuthenticated();
try { try {
const items = await client.request(readItems('products', { const items = await client.request(
fields: [ readItems('products', {
'*', fields: ['*', 'translations.*', 'categories_link.categories_id.translations.name'],
'translations.*', }),
'categories_link.categories_id.translations.name' );
] return items.map((item) => mapDirectusProduct(item, locale));
})); } catch (error) {
return items.map(item => mapDirectusProduct(item, locale)); console.error('Error fetching products:', error);
} catch (error) { return [];
console.error('Error fetching products:', error); }
return [];
}
} }
export async function getProductBySlug(slug: string, locale: string = 'de') { export async function getProductBySlug(slug: string, locale: string = 'de') {
await ensureAuthenticated(); await ensureAuthenticated();
const langCode = locale === 'en' ? 'en-US' : 'de-DE'; const langCode = locale === 'en' ? 'en-US' : 'de-DE';
try { try {
const items = await client.request(readItems('products', { const items = await client.request(
filter: { readItems('products', {
translations: { filter: {
slug: { _eq: slug }, translations: {
languages_code: { _eq: langCode } slug: { _eq: slug },
} languages_code: { _eq: langCode },
}, },
fields: [ },
'*', fields: ['*', 'translations.*', 'categories_link.categories_id.translations.name'],
'translations.*', limit: 1,
'categories_link.categories_id.translations.name' }),
], );
limit: 1
}));
if (!items || items.length === 0) return null; if (!items || items.length === 0) return null;
return mapDirectusProduct(items[0], locale); return mapDirectusProduct(items[0], locale);
} catch (error) { } catch (error) {
console.error(`Error fetching product ${slug}:`, error); console.error(`Error fetching product ${slug}:`, error);
return null; return null;
} }
} }
export async function checkHealth() { export async function checkHealth() {
try {
await ensureAuthenticated();
// 1. Basic connectivity check
await client.request(readCollections());
// 2. Schema check (does the products table exist?)
try { try {
await ensureAuthenticated(); await client.request(readItems('products', { limit: 1 }));
// Try to fetch something very simple that should exist if initialized } catch (e: any) {
await client.request(readItems('directus_collections', { limit: 1 })); if (e.message?.includes('does not exist') || e.code === 'INVALID_PAYLOAD') {
return { status: 'ok', message: 'Directus is reachable and responding.' };
} catch (error: any) {
console.error('Directus health check failed:', error);
return { return {
status: 'error', status: 'error',
message: error.message || 'Unknown error', message: 'The "products" collection is missing. Please sync your data.',
code: error.code || 'UNKNOWN' code: 'SCHEMA_MISSING',
}; };
}
throw e;
} }
return { status: 'ok', message: 'Directus is reachable and responding.' };
} catch (error: any) {
console.error('Directus health check failed:', error);
return {
status: 'error',
message: error.message || 'Unknown error',
code: error.code || 'UNKNOWN',
};
}
} }
export default client; export default client;

View File

@@ -14,7 +14,10 @@ export const envSchema = z.object({
// Analytics // Analytics
NEXT_PUBLIC_UMAMI_WEBSITE_ID: z.preprocess(preprocessEmptyString, z.string().optional()), NEXT_PUBLIC_UMAMI_WEBSITE_ID: z.preprocess(preprocessEmptyString, z.string().optional()),
NEXT_PUBLIC_UMAMI_SCRIPT_URL: z.preprocess(preprocessEmptyString, z.string().url().default('https://analytics.infra.mintel.me/script.js')), NEXT_PUBLIC_UMAMI_SCRIPT_URL: z.preprocess(
preprocessEmptyString,
z.string().url().default('https://analytics.infra.mintel.me/script.js'),
),
// Error Tracking // Error Tracking
SENTRY_DSN: z.preprocess(preprocessEmptyString, z.string().optional()), SENTRY_DSN: z.preprocess(preprocessEmptyString, z.string().optional()),
@@ -30,14 +33,18 @@ export const envSchema = z.object({
MAIL_FROM: z.preprocess(preprocessEmptyString, z.string().optional()), MAIL_FROM: z.preprocess(preprocessEmptyString, z.string().optional()),
MAIL_RECIPIENTS: z.preprocess( MAIL_RECIPIENTS: z.preprocess(
(val) => (typeof val === 'string' ? val.split(',').filter(Boolean) : val), (val) => (typeof val === 'string' ? val.split(',').filter(Boolean) : val),
z.array(z.string()).default([]) z.array(z.string()).default([]),
), ),
// Directus // Directus
DIRECTUS_URL: z.preprocess(preprocessEmptyString, z.string().url().default('http://localhost:8055')), DIRECTUS_URL: z.preprocess(
preprocessEmptyString,
z.string().url().default('http://localhost:8055'),
),
DIRECTUS_ADMIN_EMAIL: z.preprocess(preprocessEmptyString, z.string().optional()), DIRECTUS_ADMIN_EMAIL: z.preprocess(preprocessEmptyString, z.string().optional()),
DIRECTUS_ADMIN_PASSWORD: z.preprocess(preprocessEmptyString, z.string().optional()), DIRECTUS_ADMIN_PASSWORD: z.preprocess(preprocessEmptyString, z.string().optional()),
DIRECTUS_API_TOKEN: z.preprocess(preprocessEmptyString, z.string().optional()), DIRECTUS_API_TOKEN: z.preprocess(preprocessEmptyString, z.string().optional()),
INTERNAL_DIRECTUS_URL: z.preprocess(preprocessEmptyString, z.string().url().optional()),
}); });
export type Env = z.infer<typeof envSchema>; export type Env = z.infer<typeof envSchema>;
@@ -64,5 +71,6 @@ export function getRawEnv() {
DIRECTUS_ADMIN_EMAIL: process.env.DIRECTUS_ADMIN_EMAIL, DIRECTUS_ADMIN_EMAIL: process.env.DIRECTUS_ADMIN_EMAIL,
DIRECTUS_ADMIN_PASSWORD: process.env.DIRECTUS_ADMIN_PASSWORD, DIRECTUS_ADMIN_PASSWORD: process.env.DIRECTUS_ADMIN_PASSWORD,
DIRECTUS_API_TOKEN: process.env.DIRECTUS_API_TOKEN, DIRECTUS_API_TOKEN: process.env.DIRECTUS_API_TOKEN,
INTERNAL_DIRECTUS_URL: process.env.INTERNAL_DIRECTUS_URL,
}; };
} }