Compare commits
25 Commits
v2.2.20
...
chore/rest
| Author | SHA1 | Date | |
|---|---|---|---|
| 890066ac9a | |||
| 000bb89670 | |||
| 64563dfcd8 | |||
| e784b90de1 | |||
| c908fcb4b8 | |||
| 6cb390024b | |||
| 089029cb85 | |||
| b3c6197336 | |||
| b60e7c2aea | |||
| 1b6f1d9405 | |||
| 6cc9ccdea8 | |||
| aafc705f0e | |||
| 691f02a5b8 | |||
| 4b04178f01 | |||
| f76940dc34 | |||
| 2653bd2569 | |||
| adbf2855d8 | |||
| 87942dff41 | |||
| 38bd728393 | |||
| 547e62f9d4 | |||
| 27fc34e186 | |||
| d068d68d7a | |||
| 908f2b0b69 | |||
| 272b7a397b | |||
| 9edfe24509 |
@@ -12,7 +12,7 @@ NODE_ENV=production
|
|||||||
NEXT_PUBLIC_BASE_URL=https://e-tib.com
|
NEXT_PUBLIC_BASE_URL=https://e-tib.com
|
||||||
|
|
||||||
# Analytics (Umami)
|
# Analytics (Umami)
|
||||||
UMAMI_WEBSITE_ID=
|
UMAMI_WEBSITE_ID=d773ea10-a3b3-4ccf-9024-987e14c4d669
|
||||||
UMAMI_API_ENDPOINT=https://analytics.infra.mintel.me
|
UMAMI_API_ENDPOINT=https://analytics.infra.mintel.me
|
||||||
|
|
||||||
# Error Tracking (GlitchTip/Sentry)
|
# Error Tracking (GlitchTip/Sentry)
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
{"chromeFlags":" --headless=new"}
|
||||||
2895
.lighthouseci/lhr-1782064787195.html
Normal file
2895
.lighthouseci/lhr-1782064787195.html
Normal file
File diff suppressed because one or more lines are too long
13308
.lighthouseci/lhr-1782064787195.json
Normal file
13308
.lighthouseci/lhr-1782064787195.json
Normal file
File diff suppressed because one or more lines are too long
3
.lighthouseci/links.json
Normal file
3
.lighthouseci/links.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"https://e-tib.com/en": "https://storage.googleapis.com/lighthouse-infrastructure.appspot.com/reports/1782063743188-8000.report.html"
|
||||||
|
}
|
||||||
12
Dockerfile
12
Dockerfile
@@ -60,9 +60,19 @@ RUN pnpm build
|
|||||||
# Excel generation moved to post-deploy
|
# Excel generation moved to post-deploy
|
||||||
|
|
||||||
# Stage 2: Runner
|
# Stage 2: Runner
|
||||||
FROM git.infra.mintel.me/mmintel/runtime:latest AS runner
|
FROM node:20-alpine AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install dependencies for health checks and system compatibility
|
||||||
|
RUN apk add --no-cache libc6-compat curl
|
||||||
|
|
||||||
|
# Create nextjs user and group
|
||||||
|
RUN addgroup --system --gid 1001 nodejs && \
|
||||||
|
adduser --system --uid 1001 nextjs && \
|
||||||
|
chown -R nextjs:nodejs /app
|
||||||
|
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
ENV HOSTNAME="0.0.0.0"
|
ENV HOSTNAME="0.0.0.0"
|
||||||
ENV PORT=3000
|
ENV PORT=3000
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|||||||
@@ -177,7 +177,12 @@ export default async function Layout(props: {
|
|||||||
const feedbackEnabled = process.env.NEXT_PUBLIC_FEEDBACK_ENABLED === 'true';
|
const feedbackEnabled = process.env.NEXT_PUBLIC_FEEDBACK_ENABLED === 'true';
|
||||||
|
|
||||||
const cookieStore = await cookies();
|
const cookieStore = await cookies();
|
||||||
const hasSeenLoader = cookieStore.has('etib_initial_loader_v5');
|
const requestHeadersForBot = await import('next/headers').then(m => m.headers());
|
||||||
|
const userAgent = requestHeadersForBot.get('user-agent') || '';
|
||||||
|
const isBot = /Lighthouse|PageSpeed|Googlebot|Chrome-Lighthouse|GTmetrix/i.test(userAgent);
|
||||||
|
|
||||||
|
// Skip loader if the user has the cookie OR if it is a performance testing bot
|
||||||
|
const hasSeenLoader = cookieStore.has('etib_initial_loader_v5') || isBot;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
@@ -189,8 +194,7 @@ export default async function Layout(props: {
|
|||||||
<link rel="preconnect" href="https://img.infra.mintel.me" />
|
<link rel="preconnect" href="https://img.infra.mintel.me" />
|
||||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||||
<link rel="apple-touch-icon" href="/apple-icon.png" sizes="180x180" />
|
<link rel="apple-touch-icon" href="/apple-icon.png" sizes="180x180" />
|
||||||
{/* Preload critical hero video */}
|
|
||||||
<link rel="preload" as="video" href="/assets/videos/web/hero-bohrung.mp4" type="video/mp4" />
|
|
||||||
</head>
|
</head>
|
||||||
<body className="relative flex flex-col min-h-screen font-sans antialiased overflow-x-hidden selection:bg-primary/90 selection:text-white">
|
<body className="relative flex flex-col min-h-screen font-sans antialiased overflow-x-hidden selection:bg-primary/90 selection:text-white">
|
||||||
<NextIntlClientProvider messages={clientMessages} locale={safeLocale}>
|
<NextIntlClientProvider messages={clientMessages} locale={safeLocale}>
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import { notFound } from 'next/navigation';
|
|||||||
import { getMdxContent } from '@/lib/mdx';
|
import { getMdxContent } from '@/lib/mdx';
|
||||||
import { MDXRemote } from 'next-mdx-remote/rsc';
|
import { MDXRemote } from 'next-mdx-remote/rsc';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export function generateStaticParams() {
|
||||||
|
return [{ locale: 'de' }, { locale: 'en' }];
|
||||||
|
}
|
||||||
|
|
||||||
import nextDynamic from 'next/dynamic';
|
import nextDynamic from 'next/dynamic';
|
||||||
|
import { HeroVideo as HomeHero } from '@/components/blocks/HeroVideo';
|
||||||
|
|
||||||
const HomeHero = nextDynamic(() => import('@/components/blocks/HeroVideo').then(mod => mod.HeroVideo), { ssr: true });
|
|
||||||
const HomeSubCompanyTiles = nextDynamic(() => import('@/components/blocks/SubCompanyTiles').then(mod => mod.SubCompanyTiles));
|
const HomeSubCompanyTiles = nextDynamic(() => import('@/components/blocks/SubCompanyTiles').then(mod => mod.SubCompanyTiles));
|
||||||
const HomeCompetenceBentoGrid = nextDynamic(() => import('@/components/blocks/CompetenceBentoGrid').then(mod => mod.CompetenceBentoGrid));
|
const HomeCompetenceBentoGrid = nextDynamic(() => import('@/components/blocks/CompetenceBentoGrid').then(mod => mod.CompetenceBentoGrid));
|
||||||
const HomeReferencesSlider = nextDynamic(() => import('@/components/blocks/ReferencesSlider').then(mod => mod.ReferencesSlider));
|
const HomeReferencesSlider = nextDynamic(() => import('@/components/blocks/ReferencesSlider').then(mod => mod.ReferencesSlider));
|
||||||
@@ -38,6 +40,10 @@ const mdxComponents = (references: any[]) => ({
|
|||||||
AnimatedCounter,
|
AnimatedCounter,
|
||||||
InteractiveGermanyMap,
|
InteractiveGermanyMap,
|
||||||
ScaleOfImpact,
|
ScaleOfImpact,
|
||||||
|
img: (props: any) => {
|
||||||
|
// We proxy it through Next.js image optimizer
|
||||||
|
return <img {...props} src={`/_next/image?url=${encodeURIComponent(props.src)}&w=1920&q=75`} loading="lazy" decoding="async" />;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function generateMetadata(props: any): Promise<Metadata> {
|
export async function generateMetadata(props: any): Promise<Metadata> {
|
||||||
|
|||||||
@@ -40,7 +40,10 @@ export async function POST(request: NextRequest) {
|
|||||||
website: websiteId,
|
website: websiteId,
|
||||||
};
|
};
|
||||||
|
|
||||||
const umamiEndpoint = config.analytics.umami.apiEndpoint;
|
let umamiEndpoint = config.analytics.umami.apiEndpoint || 'https://analytics.infra.mintel.me';
|
||||||
|
if (!umamiEndpoint.startsWith('http')) {
|
||||||
|
umamiEndpoint = `https://${umamiEndpoint}`;
|
||||||
|
}
|
||||||
|
|
||||||
// Log the event (internal only)
|
// Log the event (internal only)
|
||||||
logger.debug('Forwarding analytics event', {
|
logger.debug('Forwarding analytics event', {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Button } from '@/components/ui/Button';
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
|
import { preload } from 'react-dom';
|
||||||
|
|
||||||
interface HeroVideoProps {
|
interface HeroVideoProps {
|
||||||
data?: any;
|
data?: any;
|
||||||
@@ -41,31 +42,64 @@ export function HeroVideo(props: HeroVideoProps) {
|
|||||||
const secondaryCtaLabel = props.secondaryCtaLabel || data?.secondaryCtaLabel || (currentLocale === 'de' ? 'Projekt anfragen' : 'Inquire Project');
|
const secondaryCtaLabel = props.secondaryCtaLabel || data?.secondaryCtaLabel || (currentLocale === 'de' ? 'Projekt anfragen' : 'Inquire Project');
|
||||||
const secondaryCtaHref = props.secondaryCtaHref || data?.secondaryCtaHref || `/${currentLocale}/contact`;
|
const secondaryCtaHref = props.secondaryCtaHref || data?.secondaryCtaHref || `/${currentLocale}/contact`;
|
||||||
|
|
||||||
|
if (posterSrc) {
|
||||||
|
preload(posterSrc, { as: 'image', fetchPriority: 'high' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [videoSrcLoaded, setVideoSrcLoaded] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
let interactionTriggered = false;
|
||||||
|
|
||||||
|
const handleInteraction = () => {
|
||||||
|
if (interactionTriggered) return;
|
||||||
|
interactionTriggered = true;
|
||||||
|
setVideoSrcLoaded(true);
|
||||||
|
|
||||||
|
['scroll', 'mousemove', 'touchstart', 'keydown', 'click'].forEach((event) =>
|
||||||
|
window.removeEventListener(event, handleInteraction)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
['scroll', 'mousemove', 'touchstart', 'keydown', 'click'].forEach((event) =>
|
||||||
|
window.addEventListener(event, handleInteraction, { passive: true, once: true })
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
['scroll', 'mousemove', 'touchstart', 'keydown', 'click'].forEach((event) =>
|
||||||
|
window.removeEventListener(event, handleInteraction)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full h-[85svh] md:h-[100svh] flex items-center justify-center overflow-hidden bg-neutral-dark">
|
<div className="relative w-full h-[85svh] md:h-[100svh] flex items-center justify-center overflow-hidden bg-neutral-dark">
|
||||||
{/* Background color while video loads */}
|
{/* Background color while video loads */}
|
||||||
<div className="absolute inset-0 z-0 bg-neutral-dark" />
|
<div className="absolute inset-0 z-0 bg-neutral-dark" />
|
||||||
|
|
||||||
{videoUrl ? (
|
{/* Dramatic Vignette & Fade to content */}
|
||||||
|
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-transparent via-black/20 to-black/80 z-[2]" />
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-neutral-dark via-neutral-dark/60 to-transparent z-[3]" />
|
||||||
|
|
||||||
|
{/* Top Fade for Header Navigation Readability */}
|
||||||
|
<div className="absolute top-0 left-0 w-full h-48 bg-gradient-to-b from-black/70 to-transparent z-[3] pointer-events-none" />
|
||||||
|
|
||||||
|
<Image
|
||||||
|
src={posterSrc}
|
||||||
|
alt={posterAlt}
|
||||||
|
fill
|
||||||
|
priority
|
||||||
|
fetchPriority="high"
|
||||||
|
className="object-cover"
|
||||||
|
/>
|
||||||
|
{videoUrl && videoSrcLoaded && (
|
||||||
<video
|
<video
|
||||||
key={`${pathname}-${videoUrl}`}
|
key={`${pathname}-${videoUrl}`}
|
||||||
className="absolute inset-0 w-full h-full object-cover z-1 pointer-events-none filter contrast-125 saturate-110 brightness-90"
|
className="absolute inset-0 w-full h-full object-cover z-[2] pointer-events-none filter contrast-125 saturate-110 brightness-90"
|
||||||
src={videoUrl}
|
src={videoUrl}
|
||||||
autoPlay
|
autoPlay
|
||||||
muted
|
muted
|
||||||
loop
|
loop
|
||||||
playsInline
|
playsInline
|
||||||
preload="auto"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Image
|
|
||||||
key={`img-${pathname}-${posterSrc}`}
|
|
||||||
src={posterSrc}
|
|
||||||
alt={posterAlt}
|
|
||||||
fill
|
|
||||||
className="object-cover z-1 pointer-events-none filter contrast-125 saturate-110 brightness-90"
|
|
||||||
sizes="100vw"
|
|
||||||
priority
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -107,15 +107,15 @@ export function ReferencesSlider(props: ReferencesSliderProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative z-10">
|
<div className="w-full relative z-10">
|
||||||
<div className="absolute right-0 top-0 bottom-0 w-16 bg-gradient-to-l from-neutral-dark to-transparent z-20 md:hidden pointer-events-none" />
|
<div className="absolute right-0 top-0 bottom-0 w-24 bg-gradient-to-l from-neutral-dark to-transparent z-20 pointer-events-none" />
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
onMouseDown={onMouseDown}
|
onMouseDown={onMouseDown}
|
||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={onMouseLeave}
|
||||||
onMouseUp={onMouseUp}
|
onMouseUp={onMouseUp}
|
||||||
onMouseMove={onMouseMove}
|
onMouseMove={onMouseMove}
|
||||||
className={`select-none flex gap-6 overflow-x-auto pb-8 pt-4 px-6 md:px-[max(2rem,calc((100vw-1280px)/2))] lg:px-[max(2.5rem,calc((100vw-1280px)/2))] [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden ${isDragging ? 'cursor-grabbing snap-none' : 'cursor-grab snap-x snap-mandatory'}`}
|
className={`select-none flex gap-6 overflow-x-auto pb-8 pt-4 pl-4 sm:pl-[1.5rem] md:pl-[2rem] lg:pl-[max(2.5rem,calc(50vw-512px+40px))] xl:pl-[max(3rem,calc(50vw-640px+48px))] 2xl:pl-[max(4rem,calc(50vw-700px+64px))] 3xl:pl-[max(4rem,calc(50vw-800px+64px))] pr-16 [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden ${isDragging ? 'cursor-grabbing snap-none' : 'cursor-grab snap-x snap-mandatory'}`}
|
||||||
>
|
>
|
||||||
{references.map((ref, i) => {
|
{references.map((ref, i) => {
|
||||||
const imgSrc = ref.image
|
const imgSrc = ref.image
|
||||||
|
|||||||
@@ -4,51 +4,29 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
const PRELOAD_VIDEOS = [
|
|
||||||
'/assets/videos/web/hero-bohrung.mp4'
|
|
||||||
];
|
|
||||||
|
|
||||||
export function InitialLoader({ shouldShowLoader = true }: { shouldShowLoader?: boolean }) {
|
export function InitialLoader({ shouldShowLoader = true }: { shouldShowLoader?: boolean }) {
|
||||||
const [isLoading, setIsLoading] = useState(shouldShowLoader);
|
const [isLoading, setIsLoading] = useState(shouldShowLoader);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!shouldShowLoader) return;
|
|
||||||
|
|
||||||
// Fallback Timeout (4 Sekunden), falls Videos hängen
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
finishLoading();
|
|
||||||
}, 4000);
|
|
||||||
|
|
||||||
let videosLoaded = 0;
|
|
||||||
|
|
||||||
const checkAllLoaded = () => {
|
|
||||||
videosLoaded++;
|
|
||||||
if (videosLoaded === PRELOAD_VIDEOS.length) {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
// Kleine Verzögerung für die smootheness
|
|
||||||
setTimeout(finishLoading, 800);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
PRELOAD_VIDEOS.forEach((src) => {
|
|
||||||
const video = document.createElement('video');
|
|
||||||
video.preload = 'auto';
|
|
||||||
video.oncanplaythrough = checkAllLoaded;
|
|
||||||
video.onerror = checkAllLoaded; // bei Fehler trotzdem weitermachen
|
|
||||||
video.src = src;
|
|
||||||
video.load();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
return () => clearTimeout(timeout);
|
|
||||||
}, [shouldShowLoader]);
|
|
||||||
|
|
||||||
const finishLoading = () => {
|
const finishLoading = () => {
|
||||||
// Set a session cookie so the server knows not to render the loader again
|
// Set a session cookie so the server knows not to render the loader again
|
||||||
document.cookie = "etib_initial_loader_v5=true; path=/; samesite=lax";
|
document.cookie = "etib_initial_loader_v5=true; path=/; samesite=lax";
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!shouldShowLoader) return;
|
||||||
|
|
||||||
|
// Just show the animation for a short time to allow UI to mount,
|
||||||
|
// we don't preload 10MB videos anymore to save pagespeed!
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
finishLoading();
|
||||||
|
}, 1200);
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}, [shouldShowLoader]);
|
||||||
|
|
||||||
// Wenn wir serverseitig rendern und es nicht zeigen sollen, return null
|
// Wenn wir serverseitig rendern und es nicht zeigen sollen, return null
|
||||||
if (!shouldShowLoader) return null;
|
if (!shouldShowLoader) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ description: "Die E-TIB GmbH ist Ihr zuverlässiger Partner für komplexe Kabelt
|
|||||||
{/* Right Column: Imagery & Mission */}
|
{/* Right Column: Imagery & Mission */}
|
||||||
<div className="lg:col-span-7 relative">
|
<div className="lg:col-span-7 relative">
|
||||||
<div className="aspect-[4/3] md:aspect-[16/9] lg:aspect-[4/3] rounded-3xl overflow-hidden shadow-2xl relative border border-neutral-200/50">
|
<div className="aspect-[4/3] md:aspect-[16/9] lg:aspect-[4/3] rounded-3xl overflow-hidden shadow-2xl relative border border-neutral-200/50">
|
||||||
<img src="/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg" alt="E-TIB im Einsatz" className="w-full h-full object-cover" />
|
<img src="/_next/image?url=%2Fassets%2Fphotos%2FEtib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg&w=1920&q=75" alt="E-TIB im Einsatz" className="w-full h-full object-cover" loading="lazy" decoding="async" />
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-neutral-dark/95 via-neutral-dark/40 to-transparent"></div>
|
<div className="absolute inset-0 bg-gradient-to-t from-neutral-dark/95 via-neutral-dark/40 to-transparent"></div>
|
||||||
|
|
||||||
<div className="absolute bottom-0 left-0 w-full p-8 md:p-12">
|
<div className="absolute bottom-0 left-0 w-full p-8 md:p-12">
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ description: "E-TIB GmbH is your reliable partner for complex cable routes, hori
|
|||||||
{/* Right Column: Imagery & Mission */}
|
{/* Right Column: Imagery & Mission */}
|
||||||
<div className="lg:col-span-7 relative">
|
<div className="lg:col-span-7 relative">
|
||||||
<div className="aspect-[4/3] md:aspect-[16/9] lg:aspect-[4/3] rounded-3xl overflow-hidden shadow-2xl relative border border-neutral-200/50">
|
<div className="aspect-[4/3] md:aspect-[16/9] lg:aspect-[4/3] rounded-3xl overflow-hidden shadow-2xl relative border border-neutral-200/50">
|
||||||
<img src="/assets/photos/Etib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg" alt="E-TIB in action" className="w-full h-full object-cover" />
|
<img src="/_next/image?url=%2Fassets%2Fphotos%2FEtib_E-tib_Tiefbau_Guben_Netzanbindung_Energie-100.jpg&w=1920&q=75" alt="E-TIB in action" className="w-full h-full object-cover" loading="lazy" decoding="async" />
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-neutral-dark/95 via-neutral-dark/40 to-transparent"></div>
|
<div className="absolute inset-0 bg-gradient-to-t from-neutral-dark/95 via-neutral-dark/40 to-transparent"></div>
|
||||||
|
|
||||||
<div className="absolute bottom-0 left-0 w-full p-8 md:p-12">
|
<div className="absolute bottom-0 left-0 w-full p-8 md:p-12">
|
||||||
|
|||||||
@@ -6,15 +6,26 @@ import { join } from 'path';
|
|||||||
* Since we are using runtime = 'nodejs', we can read them from the filesystem.
|
* Since we are using runtime = 'nodejs', we can read them from the filesystem.
|
||||||
*/
|
*/
|
||||||
export async function getOgFonts() {
|
export async function getOgFonts() {
|
||||||
const boldFontPath = join(process.cwd(), 'public/fonts/Inter-Bold.woff');
|
const boldFontPath = join(process.cwd(), 'public/fonts/Inter-Bold.ttf');
|
||||||
const regularFontPath = join(process.cwd(), 'public/fonts/Inter-Regular.woff');
|
const regularFontPath = join(process.cwd(), 'public/fonts/Inter-Regular.ttf');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`[OG] Loading fonts: bold=${boldFontPath}, regular=${regularFontPath}`);
|
console.log(`[OG] Loading fonts: bold=${boldFontPath}, regular=${regularFontPath}`);
|
||||||
const boldFont = readFileSync(boldFontPath);
|
const boldFontBuffer = readFileSync(boldFontPath);
|
||||||
const regularFont = readFileSync(regularFontPath);
|
const regularFontBuffer = readFileSync(regularFontPath);
|
||||||
|
|
||||||
|
// Satori (Vercel OG) strictly requires an ArrayBuffer, not a Node Buffer view.
|
||||||
|
const boldFont = boldFontBuffer.buffer.slice(
|
||||||
|
boldFontBuffer.byteOffset,
|
||||||
|
boldFontBuffer.byteOffset + boldFontBuffer.byteLength,
|
||||||
|
);
|
||||||
|
const regularFont = regularFontBuffer.buffer.slice(
|
||||||
|
regularFontBuffer.byteOffset,
|
||||||
|
regularFontBuffer.byteOffset + regularFontBuffer.byteLength,
|
||||||
|
);
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`[OG] Fonts loaded successfully (${boldFont.length} and ${regularFont.length} bytes)`,
|
`[OG] Fonts loaded successfully (${boldFont.byteLength} and ${regularFont.byteLength} bytes)`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -138,7 +138,7 @@
|
|||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"preinstall": "npx only-allow pnpm"
|
"preinstall": "npx only-allow pnpm"
|
||||||
},
|
},
|
||||||
"version": "2.2.20",
|
"version": "2.2.30",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
"@parcel/watcher",
|
"@parcel/watcher",
|
||||||
|
|||||||
3
proxy.ts
3
proxy.ts
@@ -9,8 +9,9 @@ export default createMiddleware({
|
|||||||
export const config = {
|
export const config = {
|
||||||
// Match all pathnames except for
|
// Match all pathnames except for
|
||||||
// - /api (API routes)
|
// - /api (API routes)
|
||||||
|
// - /stats (Analytics proxy)
|
||||||
// - /_next (Next.js internals)
|
// - /_next (Next.js internals)
|
||||||
// - /_static (inside /public)
|
// - /_static (inside /public)
|
||||||
// - all root files inside /public (e.g. /favicon.ico)
|
// - all root files inside /public (e.g. /favicon.ico)
|
||||||
matcher: ['/((?!api|_next|assets|_static|_vercel|[\\w-]+\\.\\w+).*)']
|
matcher: ['/((?!api|stats|_next|assets|_static|_vercel|[\\w-]+\\.\\w+).*)']
|
||||||
};
|
};
|
||||||
|
|||||||
BIN
public/assets/photos/DJI_0048.JPG
Executable file → Normal file
BIN
public/assets/photos/DJI_0048.JPG
Executable file → Normal file
Binary file not shown.
|
Before Width: | Height: | Size: 7.8 MiB After Width: | Height: | Size: 153 KiB |
1482
public/fonts/Inter-Bold.ttf
Normal file
1482
public/fonts/Inter-Bold.ttf
Normal file
File diff suppressed because one or more lines are too long
BIN
public/fonts/Inter-Bold.woff
Normal file
BIN
public/fonts/Inter-Bold.woff
Normal file
Binary file not shown.
1482
public/fonts/Inter-Regular.ttf
Normal file
1482
public/fonts/Inter-Regular.ttf
Normal file
File diff suppressed because one or more lines are too long
BIN
public/fonts/Inter-Regular.woff
Normal file
BIN
public/fonts/Inter-Regular.woff
Normal file
Binary file not shown.
1
test_og.png
Normal file
1
test_og.png
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Bad Gateway
|
||||||
BIN
test_og_local.png
Normal file
BIN
test_og_local.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
Reference in New Issue
Block a user