Compare commits
5 Commits
v1.2.13
...
6a0269facc
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a0269facc | |||
| 477a3bb8ce | |||
| b1859c15ce | |||
| 6085cc05dc | |||
| bcf2d60da6 |
@@ -34,6 +34,7 @@ jobs:
|
|||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: 🔐 Configure Private Registry
|
- name: 🔐 Configure Private Registry
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "Purging old build layers and dangling images..."
|
echo "Purging old build layers and dangling images..."
|
||||||
docker image prune -f
|
docker image prune -f
|
||||||
docker builder prune -f --filter "until=6h"
|
docker builder prune -f --filter "until=24h"
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -165,6 +165,7 @@ jobs:
|
|||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
|
cache: 'pnpm'
|
||||||
- name: 🔐 Registry Auth
|
- name: 🔐 Registry Auth
|
||||||
run: |
|
run: |
|
||||||
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc
|
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc
|
||||||
@@ -193,7 +194,7 @@ jobs:
|
|||||||
# ──────────────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
build:
|
build:
|
||||||
name: 🏗️ Build
|
name: 🏗️ Build
|
||||||
needs: [prepare, qa]
|
needs: [prepare]
|
||||||
if: needs.prepare.outputs.target != 'skip'
|
if: needs.prepare.outputs.target != 'skip'
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
container:
|
container:
|
||||||
@@ -212,8 +213,6 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
provenance: false
|
provenance: false
|
||||||
platforms: linux/arm64
|
platforms: linux/arm64
|
||||||
cache-from: type=gha,scope=nextjs-build-${{ needs.prepare.outputs.target }}
|
|
||||||
cache-to: type=gha,mode=max,scope=nextjs-build-${{ needs.prepare.outputs.target }}
|
|
||||||
build-args: |
|
build-args: |
|
||||||
NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_url }}
|
NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_url }}
|
||||||
NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }}
|
NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }}
|
||||||
@@ -394,7 +393,7 @@ jobs:
|
|||||||
name: 🧪 Smoke Test
|
name: 🧪 Smoke Test
|
||||||
needs: [prepare, deploy]
|
needs: [prepare, deploy]
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
if: needs.deploy.result == 'success'
|
if: needs.deploy.result == 'success' && needs.prepare.outputs.target != 'branch'
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
container:
|
container:
|
||||||
image: catthehacker/ubuntu:act-latest
|
image: catthehacker/ubuntu:act-latest
|
||||||
@@ -427,7 +426,7 @@ jobs:
|
|||||||
name: ⚡ Lighthouse
|
name: ⚡ Lighthouse
|
||||||
needs: [prepare, deploy]
|
needs: [prepare, deploy]
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
if: success() && needs.prepare.outputs.target != 'skip'
|
if: success() && needs.prepare.outputs.target != 'skip' && needs.prepare.outputs.target != 'branch'
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
container:
|
container:
|
||||||
image: catthehacker/ubuntu:act-latest
|
image: catthehacker/ubuntu:act-latest
|
||||||
@@ -442,6 +441,7 @@ jobs:
|
|||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
|
cache: 'pnpm'
|
||||||
- name: 🔐 Registry Auth
|
- name: 🔐 Registry Auth
|
||||||
run: |
|
run: |
|
||||||
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc
|
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc
|
||||||
@@ -502,7 +502,7 @@ jobs:
|
|||||||
name: ♿ WCAG
|
name: ♿ WCAG
|
||||||
needs: [prepare, deploy, smoke_test]
|
needs: [prepare, deploy, smoke_test]
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
if: success() && needs.prepare.outputs.target != 'skip'
|
if: success() && needs.prepare.outputs.target != 'skip' && needs.prepare.outputs.target != 'branch'
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
container:
|
container:
|
||||||
image: catthehacker/ubuntu:act-latest
|
image: catthehacker/ubuntu:act-latest
|
||||||
@@ -524,6 +524,7 @@ jobs:
|
|||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
|
cache: 'pnpm'
|
||||||
- name: 🔐 Registry Auth
|
- name: 🔐 Registry Auth
|
||||||
run: |
|
run: |
|
||||||
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc
|
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc
|
||||||
@@ -584,7 +585,7 @@ jobs:
|
|||||||
name: 🛡️ Quality Gates
|
name: 🛡️ Quality Gates
|
||||||
needs: [prepare, deploy, smoke_test]
|
needs: [prepare, deploy, smoke_test]
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
if: success() && needs.prepare.outputs.target != 'skip'
|
if: success() && needs.prepare.outputs.target != 'skip' && needs.prepare.outputs.target != 'branch'
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
container:
|
container:
|
||||||
image: catthehacker/ubuntu:act-latest
|
image: catthehacker/ubuntu:act-latest
|
||||||
@@ -599,6 +600,7 @@ jobs:
|
|||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
|
cache: 'pnpm'
|
||||||
- name: 🔐 Registry Auth
|
- name: 🔐 Registry Auth
|
||||||
run: |
|
run: |
|
||||||
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc
|
echo "@mintel:registry=https://${{ vars.REGISTRY_HOST || 'npm.infra.mintel.me' }}" > .npmrc
|
||||||
@@ -632,6 +634,11 @@ jobs:
|
|||||||
container:
|
container:
|
||||||
image: catthehacker/ubuntu:act-latest
|
image: catthehacker/ubuntu:act-latest
|
||||||
steps:
|
steps:
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: 'pnpm'
|
||||||
- name: 🔔 Gotify
|
- name: 🔔 Gotify
|
||||||
run: |
|
run: |
|
||||||
STATUS="${{ needs.deploy.result }}"
|
STATUS="${{ needs.deploy.result }}"
|
||||||
|
|||||||
@@ -53,11 +53,11 @@ export async function generateMetadata({ params }: ProductPageProps): Promise<Me
|
|||||||
title: categoryTitle,
|
title: categoryTitle,
|
||||||
description: categoryDesc,
|
description: categoryDesc,
|
||||||
alternates: {
|
alternates: {
|
||||||
canonical: `${SITE_URL}/${locale}/products/${productSlug}`,
|
canonical: `${SITE_URL}/${locale}/${await mapFileSlugToTranslated('products', locale)}/${productSlug}`,
|
||||||
languages: {
|
languages: {
|
||||||
de: `${SITE_URL}/de/products/${await mapFileSlugToTranslated(productSlug, 'de')}`,
|
de: `${SITE_URL}/de/${await mapFileSlugToTranslated('products', 'de')}/${await mapFileSlugToTranslated(productSlug, 'de')}`,
|
||||||
en: `${SITE_URL}/en/products/${await mapFileSlugToTranslated(productSlug, 'en')}`,
|
en: `${SITE_URL}/en/${await mapFileSlugToTranslated('products', 'en')}/${await mapFileSlugToTranslated(productSlug, 'en')}`,
|
||||||
'x-default': `${SITE_URL}/en/products/${await mapFileSlugToTranslated(productSlug, 'en')}`,
|
'x-default': `${SITE_URL}/en/${await mapFileSlugToTranslated('products', 'en')}/${await mapFileSlugToTranslated(productSlug, 'en')}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
@@ -81,11 +81,11 @@ export async function generateMetadata({ params }: ProductPageProps): Promise<Me
|
|||||||
title: product.frontmatter.title,
|
title: product.frontmatter.title,
|
||||||
description: product.frontmatter.description,
|
description: product.frontmatter.description,
|
||||||
alternates: {
|
alternates: {
|
||||||
canonical: `${SITE_URL}/${locale}/products/${slug.join('/')}`,
|
canonical: `${SITE_URL}/${locale}/${await mapFileSlugToTranslated('products', locale)}/${slug.join('/')}`,
|
||||||
languages: {
|
languages: {
|
||||||
de: `${SITE_URL}/de/products/${await mapFileSlugToTranslated(slug[0], 'de')}/${await mapFileSlugToTranslated(productSlug, 'de')}`,
|
de: `${SITE_URL}/de/${await mapFileSlugToTranslated('products', 'de')}/${await mapFileSlugToTranslated(slug[0], 'de')}/${await mapFileSlugToTranslated(productSlug, 'de')}`,
|
||||||
en: `${SITE_URL}/en/products/${await mapFileSlugToTranslated(slug[0], 'en')}/${await mapFileSlugToTranslated(productSlug, 'en')}`,
|
en: `${SITE_URL}/en/${await mapFileSlugToTranslated('products', 'en')}/${await mapFileSlugToTranslated(slug[0], 'en')}/${await mapFileSlugToTranslated(productSlug, 'en')}`,
|
||||||
'x-default': `${SITE_URL}/en/products/${await mapFileSlugToTranslated(slug[0], 'en')}/${await mapFileSlugToTranslated(productSlug, 'en')}`,
|
'x-default': `${SITE_URL}/en/${await mapFileSlugToTranslated('products', 'en')}/${await mapFileSlugToTranslated(slug[0], 'en')}/${await mapFileSlugToTranslated(productSlug, 'en')}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
@@ -179,6 +179,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
setRequestLocale(locale);
|
setRequestLocale(locale);
|
||||||
const productSlug = slug[slug.length - 1];
|
const productSlug = slug[slug.length - 1];
|
||||||
const t = await getTranslations('Products');
|
const t = await getTranslations('Products');
|
||||||
|
const productsSlug = await mapFileSlugToTranslated('products', locale);
|
||||||
|
|
||||||
// Check if it's a category page
|
// Check if it's a category page
|
||||||
const categories = [
|
const categories = [
|
||||||
@@ -220,7 +221,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
<div className="max-w-4xl animate-slide-up">
|
<div className="max-w-4xl animate-slide-up">
|
||||||
<nav className="flex items-center mb-8 text-white/40 text-sm font-bold uppercase tracking-widest">
|
<nav className="flex items-center mb-8 text-white/40 text-sm font-bold uppercase tracking-widest">
|
||||||
<Link
|
<Link
|
||||||
href={`/${locale}/${await mapFileSlugToTranslated('products', locale)}`}
|
href={`/${locale}/${productsSlug}`}
|
||||||
className="hover:text-accent transition-colors"
|
className="hover:text-accent transition-colors"
|
||||||
>
|
>
|
||||||
{t.has('breadcrumb') ? t('breadcrumb') : 'Products'}
|
{t.has('breadcrumb') ? t('breadcrumb') : 'Products'}
|
||||||
@@ -242,7 +243,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
{productsWithTranslatedSlugs.map((product) => (
|
{productsWithTranslatedSlugs.map((product) => (
|
||||||
<Link
|
<Link
|
||||||
key={product.slug}
|
key={product.slug}
|
||||||
href={`/${locale}/products/${productSlug}/${product.translatedSlug}`}
|
href={`/${locale}/${productsSlug}/${productSlug}/${product.translatedSlug}`}
|
||||||
className="group block bg-white rounded-[32px] overflow-hidden shadow-sm hover:shadow-2xl transition-all duration-500 border border-neutral-dark/5"
|
className="group block bg-white rounded-[32px] overflow-hidden shadow-sm hover:shadow-2xl transition-all duration-500 border border-neutral-dark/5"
|
||||||
>
|
>
|
||||||
<Card tag="article" className="premium-card-reset">
|
<Card tag="article" className="premium-card-reset">
|
||||||
@@ -381,14 +382,14 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
<div className="max-w-4xl animate-slide-up">
|
<div className="max-w-4xl animate-slide-up">
|
||||||
<nav className="flex items-center mb-12 text-white/40 text-[10px] font-black uppercase tracking-[0.2em]">
|
<nav className="flex items-center mb-12 text-white/40 text-[10px] font-black uppercase tracking-[0.2em]">
|
||||||
<Link
|
<Link
|
||||||
href={`/${locale}/${await mapFileSlugToTranslated('products', locale)}`}
|
href={`/${locale}/${productsSlug}`}
|
||||||
className="hover:text-accent transition-colors"
|
className="hover:text-accent transition-colors"
|
||||||
>
|
>
|
||||||
{t.has('breadcrumb') ? t('breadcrumb') : 'Products'}
|
{t.has('breadcrumb') ? t('breadcrumb') : 'Products'}
|
||||||
</Link>
|
</Link>
|
||||||
<span className="mx-4 opacity-20">/</span>
|
<span className="mx-4 opacity-20">/</span>
|
||||||
<Link
|
<Link
|
||||||
href={`/${locale}/products/${categorySlug}`}
|
href={`/${locale}/${productsSlug}/${categorySlug}`}
|
||||||
className="hover:text-accent transition-colors"
|
className="hover:text-accent transition-colors"
|
||||||
>
|
>
|
||||||
{categoryTitle}
|
{categoryTitle}
|
||||||
@@ -511,7 +512,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
'@type': 'Offer',
|
'@type': 'Offer',
|
||||||
availability: 'https://schema.org/InStock',
|
availability: 'https://schema.org/InStock',
|
||||||
priceCurrency: 'EUR',
|
priceCurrency: 'EUR',
|
||||||
url: `${SITE_URL}/${locale}/products/${slug.join('/')}`,
|
url: `${SITE_URL}/${locale}/${await mapFileSlugToTranslated('products', locale)}/${slug.join('/')}`,
|
||||||
itemCondition: 'https://schema.org/NewCondition',
|
itemCondition: 'https://schema.org/NewCondition',
|
||||||
},
|
},
|
||||||
additionalProperty: technicalItems.map((item: any) => ({
|
additionalProperty: technicalItems.map((item: any) => ({
|
||||||
@@ -522,7 +523,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
category: product.frontmatter.categories.join(', '),
|
category: product.frontmatter.categories.join(', '),
|
||||||
mainEntityOfPage: {
|
mainEntityOfPage: {
|
||||||
'@type': 'WebPage',
|
'@type': 'WebPage',
|
||||||
'@id': `${SITE_URL}/${locale}/products/${slug.join('/')}`,
|
'@id': `${SITE_URL}/${locale}/${await mapFileSlugToTranslated('products', locale)}/${slug.join('/')}`,
|
||||||
},
|
},
|
||||||
} as any
|
} as any
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { getAllProducts } from '@/lib/mdx';
|
|||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { RelatedProductLink } from './RelatedProductLink';
|
import { RelatedProductLink } from './RelatedProductLink';
|
||||||
|
import { mapFileSlugToTranslated } from '@/lib/slugs';
|
||||||
|
|
||||||
interface RelatedProductsProps {
|
interface RelatedProductsProps {
|
||||||
currentSlug: string;
|
currentSlug: string;
|
||||||
@@ -16,6 +17,7 @@ export default async function RelatedProducts({
|
|||||||
}: RelatedProductsProps) {
|
}: RelatedProductsProps) {
|
||||||
const products = await getAllProducts(locale);
|
const products = await getAllProducts(locale);
|
||||||
const t = await getTranslations('Products');
|
const t = await getTranslations('Products');
|
||||||
|
const productsSlug = await mapFileSlugToTranslated('products', locale);
|
||||||
|
|
||||||
// Filter products: same category, not current product
|
// Filter products: same category, not current product
|
||||||
const related = products
|
const related = products
|
||||||
@@ -27,6 +29,34 @@ export default async function RelatedProducts({
|
|||||||
|
|
||||||
if (related.length === 0) return null;
|
if (related.length === 0) return null;
|
||||||
|
|
||||||
|
// Pre-calculate translated slugs for related products
|
||||||
|
const relatedWithTranslatedSlugs = await Promise.all(
|
||||||
|
related.map(async (product) => {
|
||||||
|
// Find the category slug for the link
|
||||||
|
const categorySlugs = [
|
||||||
|
'low-voltage-cables',
|
||||||
|
'medium-voltage-cables',
|
||||||
|
'high-voltage-cables',
|
||||||
|
'solar-cables',
|
||||||
|
];
|
||||||
|
const catFileSlug =
|
||||||
|
categorySlugs.find((slug) => {
|
||||||
|
const key = slug.replace(/-cables$/, '').replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
||||||
|
const title = t(`categories.${key}.title`);
|
||||||
|
return product.frontmatter.categories.some(
|
||||||
|
(cat) => cat.toLowerCase().replace(/\s+/g, '-') === slug || cat === title,
|
||||||
|
);
|
||||||
|
}) || 'low-voltage-cables';
|
||||||
|
|
||||||
|
const catSlug = await mapFileSlugToTranslated(catFileSlug, locale);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...product,
|
||||||
|
catSlug,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="">
|
<div className="">
|
||||||
<div className="flex items-end justify-between mb-12">
|
<div className="flex items-end justify-between mb-12">
|
||||||
@@ -39,29 +69,11 @@ export default async function RelatedProducts({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||||
{related.map((product) => {
|
{relatedWithTranslatedSlugs.map((product) => {
|
||||||
// Find the category slug for the link
|
|
||||||
const categorySlugs = [
|
|
||||||
'low-voltage-cables',
|
|
||||||
'medium-voltage-cables',
|
|
||||||
'high-voltage-cables',
|
|
||||||
'solar-cables',
|
|
||||||
];
|
|
||||||
const catSlug =
|
|
||||||
categorySlugs.find((slug) => {
|
|
||||||
const key = slug
|
|
||||||
.replace(/-cables$/, '')
|
|
||||||
.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
|
||||||
const title = t(`categories.${key}.title`);
|
|
||||||
return product.frontmatter.categories.some(
|
|
||||||
(cat) => cat.toLowerCase().replace(/\s+/g, '-') === slug || cat === title,
|
|
||||||
);
|
|
||||||
}) || 'low-voltage-cables';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RelatedProductLink
|
<RelatedProductLink
|
||||||
key={product.slug}
|
key={product.slug}
|
||||||
href={`/${locale}/products/${catSlug}/${product.slug}`}
|
href={`/${locale}/${productsSlug}/${product.catSlug}/${product.slug}`}
|
||||||
productSlug={product.slug}
|
productSlug={product.slug}
|
||||||
productTitle={product.frontmatter.title}
|
productTitle={product.frontmatter.title}
|
||||||
className="group block bg-white rounded-[32px] overflow-hidden shadow-sm hover:shadow-2xl transition-all duration-500 border border-neutral-dark/5"
|
className="group block bg-white rounded-[32px] overflow-hidden shadow-sm hover:shadow-2xl transition-all duration-500 border border-neutral-dark/5"
|
||||||
|
|||||||
@@ -155,7 +155,9 @@ services:
|
|||||||
- default
|
- default
|
||||||
|
|
||||||
klz-imgproxy:
|
klz-imgproxy:
|
||||||
image: registry.infra.mintel.me/mintel/image-processor:latest
|
build:
|
||||||
|
context: ../at-mintel
|
||||||
|
dockerfile: apps/image-service/Dockerfile
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- default
|
- default
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ async function isImgproxyDown() {
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - imgproxyStatus.lastCheck > 60000) {
|
if (now - imgproxyStatus.lastCheck > 60000) {
|
||||||
try {
|
try {
|
||||||
const imgproxyUrl = process.env.IMGPROXY_URL || 'https://img.infra.mintel.me';
|
const imgproxyUrl = process.env.IMGPROXY_URL || 'http://klz-imgproxy:8080';
|
||||||
const checkUrl = imgproxyUrl.startsWith('http') ? imgproxyUrl : `https://${imgproxyUrl}`;
|
const checkUrl = imgproxyUrl.startsWith('http') ? imgproxyUrl : `https://${imgproxyUrl}`;
|
||||||
const res = await fetch(checkUrl, { signal: AbortSignal.timeout(2000) });
|
const res = await fetch(checkUrl, { signal: AbortSignal.timeout(2000) });
|
||||||
imgproxyStatus.isDown = res.status >= 500;
|
imgproxyStatus.isDown = res.status >= 500;
|
||||||
|
|||||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/types/routes.d.ts";
|
import "./.next/dev/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const nextConfig = {
|
|||||||
async headers() {
|
async headers() {
|
||||||
const umamiDomain = new URL(process.env.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me').origin;
|
const umamiDomain = new URL(process.env.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me').origin;
|
||||||
const directusDomain = new URL(process.env.DIRECTUS_URL || 'https://cms.klz-cables.com').origin;
|
const directusDomain = new URL(process.env.DIRECTUS_URL || 'https://cms.klz-cables.com').origin;
|
||||||
const imgproxyDomain = new URL(process.env.IMGPROXY_URL || 'https://img.infra.mintel.me').origin;
|
const imgproxyDomain = new URL(process.env.IMGPROXY_URL || 'http://klz-imgproxy:8080').origin;
|
||||||
const glitchtipDomain = new URL(process.env.SENTRY_DSN ? new URL(process.env.SENTRY_DSN).origin : 'https://errors.infra.mintel.me').origin;
|
const glitchtipDomain = new URL(process.env.SENTRY_DSN ? new URL(process.env.SENTRY_DSN).origin : 'https://errors.infra.mintel.me').origin;
|
||||||
|
|
||||||
const cspHeader = `
|
const cspHeader = `
|
||||||
@@ -395,7 +395,7 @@ const nextConfig = {
|
|||||||
|
|
||||||
const directusUrl = process.env.INTERNAL_DIRECTUS_URL || process.env.DIRECTUS_URL || 'https://cms.klz-cables.com';
|
const directusUrl = process.env.INTERNAL_DIRECTUS_URL || process.env.DIRECTUS_URL || 'https://cms.klz-cables.com';
|
||||||
|
|
||||||
let imgproxyUrl = process.env.IMGPROXY_URL || 'https://img.infra.mintel.me';
|
let imgproxyUrl = process.env.IMGPROXY_URL || 'http://klz-imgproxy:8080';
|
||||||
if (!imgproxyUrl.startsWith('http')) {
|
if (!imgproxyUrl.startsWith('http')) {
|
||||||
imgproxyUrl = `https://${imgproxyUrl}`;
|
imgproxyUrl = `https://${imgproxyUrl}`;
|
||||||
}
|
}
|
||||||
@@ -405,6 +405,10 @@ const nextConfig = {
|
|||||||
source: '/de/produkte',
|
source: '/de/produkte',
|
||||||
destination: '/de/products',
|
destination: '/de/products',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
source: '/de/produkte/:path*',
|
||||||
|
destination: '/de/products/:path*',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
source: '/cms/:path*',
|
source: '/cms/:path*',
|
||||||
destination: `${directusUrl}/:path*`,
|
destination: `${directusUrl}/:path*`,
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ REMOTE_HOST="root@alpha.mintel.me"
|
|||||||
REMOTE_DIR="/home/deploy/sites/klz-cables.com"
|
REMOTE_DIR="/home/deploy/sites/klz-cables.com"
|
||||||
|
|
||||||
# DB Details (matching docker-compose defaults)
|
# DB Details (matching docker-compose defaults)
|
||||||
DB_USER="klz_db_user"
|
LOCAL_DB_USER="klz_db_user"
|
||||||
|
REMOTE_DB_USER="directus"
|
||||||
DB_NAME="directus"
|
DB_NAME="directus"
|
||||||
|
|
||||||
ACTION=$1
|
ACTION=$1
|
||||||
@@ -26,19 +27,9 @@ fi
|
|||||||
|
|
||||||
# Map Environment to Project Name
|
# Map Environment to Project Name
|
||||||
case $ENV in
|
case $ENV in
|
||||||
testing)
|
production|staging|testing)
|
||||||
PROJECT_NAME="klz-cables-testing"
|
PROJECT_NAME="klz-cablescom"
|
||||||
ENV_FILE=".env.testing"
|
ENV_FILE=".env"
|
||||||
;;
|
|
||||||
staging)
|
|
||||||
PROJECT_NAME="klz-cables-staging"
|
|
||||||
ENV_FILE=".env.staging"
|
|
||||||
;;
|
|
||||||
production)
|
|
||||||
PROJECT_NAME="klz-cables-prod"
|
|
||||||
# Fallback to older project name if prod-specific one isn't found later in the script
|
|
||||||
OLD_PROJECT_NAME="klz-cablescom"
|
|
||||||
ENV_FILE=".env.prod"
|
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "❌ Invalid environment: $ENV. Use testing, staging, or production."
|
echo "❌ Invalid environment: $ENV. Use testing, staging, or production."
|
||||||
@@ -82,10 +73,10 @@ if [ "$ACTION" == "push" ]; then
|
|||||||
|
|
||||||
# Wipe remote DB clean before restore to avoid constraint errors
|
# Wipe remote DB clean before restore to avoid constraint errors
|
||||||
echo "🧹 Wiping remote database schema..."
|
echo "🧹 Wiping remote database schema..."
|
||||||
ssh "$REMOTE_HOST" "docker exec $REMOTE_DB_CONTAINER psql -U $DB_USER $DB_NAME -c 'DROP SCHEMA public CASCADE; CREATE SCHEMA public;'"
|
ssh "$REMOTE_HOST" "docker exec $REMOTE_DB_CONTAINER psql -U $REMOTE_DB_USER $DB_NAME -c 'DROP SCHEMA public CASCADE; CREATE SCHEMA public;'"
|
||||||
|
|
||||||
echo "⚡ Restoring database..."
|
echo "⚡ Restoring database..."
|
||||||
ssh "$REMOTE_HOST" "docker exec -i $REMOTE_DB_CONTAINER psql -U $DB_USER $DB_NAME < $REMOTE_DIR/dump.sql"
|
ssh "$REMOTE_HOST" "docker exec -i $REMOTE_DB_CONTAINER psql -U $REMOTE_DB_USER $DB_NAME < $REMOTE_DIR/dump.sql"
|
||||||
|
|
||||||
# 4. Sync Uploads
|
# 4. Sync Uploads
|
||||||
echo "📁 Syncing uploads (Local -> $ENV)..."
|
echo "📁 Syncing uploads (Local -> $ENV)..."
|
||||||
@@ -106,19 +97,14 @@ if [ "$ACTION" == "push" ]; then
|
|||||||
elif [ "$ACTION" == "pull" ]; then
|
elif [ "$ACTION" == "pull" ]; then
|
||||||
echo "📥 Pulling $ENV Data to Local..."
|
echo "📥 Pulling $ENV Data to Local..."
|
||||||
|
|
||||||
# 1. DB Dump on Remote
|
# The remote service name is 'klz-db' according to docker compose config
|
||||||
echo "📦 Dumping remote database ($ENV)..."
|
REMOTE_DB_CONTAINER=$(ssh "$REMOTE_HOST" "cd $REMOTE_DIR && docker compose -p $PROJECT_NAME ps -q klz-db")
|
||||||
REMOTE_DB_CONTAINER=$(ssh "$REMOTE_HOST" "cd $REMOTE_DIR && docker compose -p $PROJECT_NAME ps -q directus-db")
|
|
||||||
if [ -z "$REMOTE_DB_CONTAINER" ] && [ -n "$OLD_PROJECT_NAME" ]; then
|
|
||||||
echo "⚠️ $PROJECT_NAME not found, trying fallback $OLD_PROJECT_NAME..."
|
|
||||||
REMOTE_DB_CONTAINER=$(ssh "$REMOTE_HOST" "cd $REMOTE_DIR && docker compose -p $OLD_PROJECT_NAME ps -q directus-db")
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$REMOTE_DB_CONTAINER" ]; then
|
if [ -z "$REMOTE_DB_CONTAINER" ]; then
|
||||||
echo "❌ Remote $ENV-db container not found!"
|
echo "❌ Remote $ENV-db container not found!"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
ssh "$REMOTE_HOST" "docker exec $REMOTE_DB_CONTAINER pg_dump -U $DB_USER --clean --if-exists --no-owner --no-privileges $DB_NAME > $REMOTE_DIR/dump.sql"
|
ssh "$REMOTE_HOST" "docker exec $REMOTE_DB_CONTAINER pg_dump -U $REMOTE_DB_USER --clean --if-exists --no-owner --no-privileges $DB_NAME > $REMOTE_DIR/dump.sql"
|
||||||
|
|
||||||
# 2. Download Dump
|
# 2. Download Dump
|
||||||
echo "📥 Downloading dump..."
|
echo "📥 Downloading dump..."
|
||||||
@@ -126,10 +112,10 @@ elif [ "$ACTION" == "pull" ]; then
|
|||||||
|
|
||||||
# Wipe local DB clean before restore to avoid constraint errors
|
# Wipe local DB clean before restore to avoid constraint errors
|
||||||
echo "🧹 Wiping local database schema..."
|
echo "🧹 Wiping local database schema..."
|
||||||
docker exec "$LOCAL_DB_CONTAINER" psql -U "$DB_USER" "$DB_NAME" -c 'DROP SCHEMA public CASCADE; CREATE SCHEMA public;'
|
docker exec "$LOCAL_DB_CONTAINER" psql -U "$LOCAL_DB_USER" "$DB_NAME" -c 'DROP SCHEMA public CASCADE; CREATE SCHEMA public;'
|
||||||
|
|
||||||
echo "⚡ Restoring database locally..."
|
echo "⚡ Restoring database locally..."
|
||||||
docker exec -i "$LOCAL_DB_CONTAINER" psql -U "$DB_USER" "$DB_NAME" < dump.sql
|
docker exec -i "$LOCAL_DB_CONTAINER" psql -U "$LOCAL_DB_USER" "$DB_NAME" < dump.sql
|
||||||
|
|
||||||
# 4. Sync Uploads
|
# 4. Sync Uploads
|
||||||
echo "📁 Syncing uploads ($ENV -> Local)..."
|
echo "📁 Syncing uploads ($ENV -> Local)..."
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
|
@config "../tailwind.config.cjs";
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--font-sans:
|
--font-sans:
|
||||||
@@ -46,6 +47,18 @@
|
|||||||
--animate-slight-fade-in-from-bottom: slight-fade-in-from-bottom 0.8s
|
--animate-slight-fade-in-from-bottom: slight-fade-in-from-bottom 0.8s
|
||||||
cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||||
--animate-gradient-x: gradient-x 15s ease infinite;
|
--animate-gradient-x: gradient-x 15s ease infinite;
|
||||||
|
--animate-draw-stroke: draw-stroke 1.8s ease-in-out 0.5s forwards;
|
||||||
|
|
||||||
|
@keyframes draw-stroke {
|
||||||
|
from {
|
||||||
|
stroke-dasharray: 1;
|
||||||
|
stroke-dashoffset: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
stroke-dasharray: 1;
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes gradient-x {
|
@keyframes gradient-x {
|
||||||
0%,
|
0%,
|
||||||
|
|||||||
32
turbo.json
32
turbo.json
@@ -1,19 +1,51 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://turbo.build/schema.json",
|
"$schema": "https://turbo.build/schema.json",
|
||||||
|
"globalDependencies": [
|
||||||
|
"pnpm-lock.yaml",
|
||||||
|
".gitea/workflows/ci.yml",
|
||||||
|
".gitea/workflows/deploy.yml"
|
||||||
|
],
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"lint": {
|
"lint": {
|
||||||
|
"inputs": [
|
||||||
|
"app/**/*.tsx",
|
||||||
|
"app/**/*.ts",
|
||||||
|
"components/**/*.tsx",
|
||||||
|
"components/**/*.ts",
|
||||||
|
"lib/**/*.ts",
|
||||||
|
"eslint.config.mjs"
|
||||||
|
],
|
||||||
"outputs": []
|
"outputs": []
|
||||||
},
|
},
|
||||||
"typecheck": {
|
"typecheck": {
|
||||||
|
"inputs": [
|
||||||
|
"app/**/*.tsx",
|
||||||
|
"app/**/*.ts",
|
||||||
|
"components/**/*.tsx",
|
||||||
|
"components/**/*.ts",
|
||||||
|
"lib/**/*.ts",
|
||||||
|
"tsconfig.json"
|
||||||
|
],
|
||||||
"outputs": []
|
"outputs": []
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
|
"inputs": [
|
||||||
|
"app/**/*.tsx",
|
||||||
|
"app/**/*.ts",
|
||||||
|
"components/**/*.tsx",
|
||||||
|
"components/**/*.ts",
|
||||||
|
"lib/**/*.ts",
|
||||||
|
"tests/**/*.ts",
|
||||||
|
"vitest.config.mts"
|
||||||
|
],
|
||||||
"outputs": []
|
"outputs": []
|
||||||
},
|
},
|
||||||
"check:spell": {
|
"check:spell": {
|
||||||
|
"inputs": ["content/**/*.{md,mdx}", "app/**/*.tsx", "components/**/*.tsx", "cspell.json"],
|
||||||
"outputs": []
|
"outputs": []
|
||||||
},
|
},
|
||||||
"check:mdx": {
|
"check:mdx": {
|
||||||
|
"inputs": ["content/**/*.{md,mdx}", "scripts/validate-mdx.mjs"],
|
||||||
"outputs": []
|
"outputs": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user