fix(web): remove redundant prop-types and unblock lint pipeline
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Failing after 2m24s
Build & Deploy / 🏗️ Build (push) Failing after 3m40s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s

This commit is contained in:
2026-02-24 11:38:43 +01:00
parent 95a8b702fe
commit 6864903cff
205 changed files with 6570 additions and 1324 deletions

1
.gitignore vendored
View File

@@ -3,6 +3,7 @@ dist/
.next/ .next/
out/ out/
.contentlayer/ .contentlayer/
.pnpm-store
# generated types # generated types
.astro/ .astro/

View File

@@ -5,14 +5,12 @@ WORKDIR /app
# Arguments for build-time configuration # Arguments for build-time configuration
ARG NEXT_PUBLIC_BASE_URL ARG NEXT_PUBLIC_BASE_URL
ARG NEXT_PUBLIC_TARGET ARG NEXT_PUBLIC_TARGET
ARG DIRECTUS_URL
ARG UMAMI_API_ENDPOINT ARG UMAMI_API_ENDPOINT
ARG NPM_TOKEN ARG NPM_TOKEN
# Environment variables for Next.js build # Environment variables for Next.js build
ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
ENV NEXT_PUBLIC_TARGET=$NEXT_PUBLIC_TARGET ENV NEXT_PUBLIC_TARGET=$NEXT_PUBLIC_TARGET
ENV DIRECTUS_URL=$DIRECTUS_URL
ENV UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT ENV UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT
ENV SKIP_RUNTIME_ENV_VALIDATION=true ENV SKIP_RUNTIME_ENV_VALIDATION=true
ENV CI=true ENV CI=true

20
Dockerfile.dev Normal file
View File

@@ -0,0 +1,20 @@
FROM node:20-alpine
# Install essential build tools if needed (e.g., for node-gyp)
RUN apk add --no-cache libc6-compat python3 make g++
WORKDIR /app
# Enable corepack for pnpm
RUN corepack enable
# Pre-set the pnpm store directory to a location we can volume-mount
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
# Set up pnpm store configuration
RUN pnpm config set store-dir /pnpm/store
# Note: Dependency installation happens at runtime to support linked packages
# and named volumes, but the base image is now optimized for the stack.
EXPOSE 3000

95
Posts.ts.tmp Normal file
View File

@@ -0,0 +1,95 @@
import type { CollectionConfig } from "payload";
import { lexicalEditor, BlocksFeature } from "@payloadcms/richtext-lexical";
import { allBlocks } from "../blocks/allBlocks";
export const Posts: CollectionConfig = {
slug: "posts",
admin: {
useAsTitle: "title",
},
access: {
read: () => true, // Publicly readable API
},
fields: [
{
name: "aiOptimizer",
type: "ui",
admin: {
position: "sidebar",
components: {
Field: "@/src/payload/components/OptimizeButton#OptimizeButton",
},
},
},
{
name: "title",
type: "text",
required: true,
},
{
name: "slug",
type: "text",
required: true,
unique: true,
admin: {
position: "sidebar",
},
hooks: {
beforeValidate: [
({ value, data }) => {
if (value) return value;
if (data?.title) {
return data.title
.toLowerCase()
.replace(/ /g, "-")
.replace(/[^\w-]+/g, "");
}
return value;
},
],
},
},
{
name: "description",
type: "text",
required: true,
},
{
name: "date",
type: "date",
required: true,
},
{
name: "tags",
type: "array",
required: true,
fields: [
{
name: "tag",
type: "text",
},
],
},
{
name: "featuredImage",
type: "upload",
relationTo: "media",
admin: {
description: "The main hero image for the blog post.",
position: "sidebar",
},
},
{
name: "content",
type: "richText",
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
BlocksFeature({
blocks: allBlocks,
}),
],
}),
},
],
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 953 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 915 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 987 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 670 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 987 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 690 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 743 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 792 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 891 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 783 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 868 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 633 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 752 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 620 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 575 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 966 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 620 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB

View File

@@ -1,4 +1,12 @@
"use server"; "use server";
import { handleServerFunctions as payloadHandleServerFunctions } from "@payloadcms/next/layouts"; import { handleServerFunctions as payloadHandleServerFunctions } from "@payloadcms/next/layouts";
import config from "@payload-config";
import { importMap } from "./admin/importMap";
export const handleServerFunctions = payloadHandleServerFunctions; export const handleServerFunctions = async (args: any) => {
return payloadHandleServerFunctions({
...args,
config,
importMap,
});
};

View File

@@ -1,6 +1,99 @@
import { OptimizeButton as OptimizeButton_a629b3460534b7aa208597fdc5e30aec } from "@/src/payload/components/OptimizeButton";
import { GenerateSlugButton as GenerateSlugButton_63aadb132a046b3f001fac7a715e5717 } from "@/src/payload/components/FieldGenerators/GenerateSlugButton";
import { default as default_76cec558bd86098fa1dab70b12eb818f } from "@/src/payload/components/TagSelector";
import { GenerateThumbnailButton as GenerateThumbnailButton_39d416c162062cbe7173a99e3239786e } from "@/src/payload/components/FieldGenerators/GenerateThumbnailButton";
import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from "@payloadcms/richtext-lexical/rsc";
import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from "@payloadcms/richtext-lexical/rsc";
import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from "@payloadcms/richtext-lexical/rsc";
import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { AiFieldButton as AiFieldButton_da42292f87769a8025025b774910be6d } from "@/src/payload/components/FieldGenerators/AiFieldButton";
import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { BlockquoteFeatureClient as BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { RelationshipFeatureClient as RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { ChecklistFeatureClient as ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { OrderedListFeatureClient as OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { IndentFeatureClient as IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { InlineCodeFeatureClient as InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { SuperscriptFeatureClient as SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { SubscriptFeatureClient as SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from "@payloadcms/richtext-lexical/client";
import { default as default_2ebf44fdf8ebc607cf0de30cff485248 } from "@/src/payload/components/ColorPicker";
import { default as default_a1c6da8fb7dd9846a8b07123ff256d09 } from "@/src/payload/components/IconSelector";
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from "@payloadcms/next/rsc"; import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from "@payloadcms/next/rsc";
export const importMap = { export const importMap = {
"@/src/payload/components/OptimizeButton#OptimizeButton":
OptimizeButton_a629b3460534b7aa208597fdc5e30aec,
"@/src/payload/components/FieldGenerators/GenerateSlugButton#GenerateSlugButton":
GenerateSlugButton_63aadb132a046b3f001fac7a715e5717,
"@/src/payload/components/TagSelector#default":
default_76cec558bd86098fa1dab70b12eb818f,
"@/src/payload/components/FieldGenerators/GenerateThumbnailButton#GenerateThumbnailButton":
GenerateThumbnailButton_39d416c162062cbe7173a99e3239786e,
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell":
RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalField":
RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
"@payloadcms/richtext-lexical/rsc#LexicalDiffComponent":
LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e,
"@payloadcms/richtext-lexical/client#BlocksFeatureClient":
BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@/src/payload/components/FieldGenerators/AiFieldButton#AiFieldButton":
AiFieldButton_da42292f87769a8025025b774910be6d,
"@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient":
InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient":
HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#UploadFeatureClient":
UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#BlockquoteFeatureClient":
BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#RelationshipFeatureClient":
RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#LinkFeatureClient":
LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#ChecklistFeatureClient":
ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#OrderedListFeatureClient":
OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#UnorderedListFeatureClient":
UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#IndentFeatureClient":
IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#AlignFeatureClient":
AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#HeadingFeatureClient":
HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#ParagraphFeatureClient":
ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#InlineCodeFeatureClient":
InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#SuperscriptFeatureClient":
SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#SubscriptFeatureClient":
SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#StrikethroughFeatureClient":
StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient":
UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#BoldFeatureClient":
BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#ItalicFeatureClient":
ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@/src/payload/components/ColorPicker#default":
default_2ebf44fdf8ebc607cf0de30cff485248,
"@/src/payload/components/IconSelector#default":
default_a1c6da8fb7dd9846a8b07123ff256d09,
"@payloadcms/next/rsc#CollectionCards": "@payloadcms/next/rsc#CollectionCards":
CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1, CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1,
}; };

View File

@@ -31,7 +31,7 @@ import {
CodeSnippet, CodeSnippet,
AbstractCircuit, AbstractCircuit,
} from "@/src/components/Effects"; } from "@/src/components/Effects";
import { getImgproxyUrl } from "@/src/utils/imgproxy";
import { Marker } from "@/src/components/Marker"; import { Marker } from "@/src/components/Marker";
export default function AboutPage() { export default function AboutPage() {
@@ -51,12 +51,7 @@ export default function AboutPage() {
<div className="relative w-32 h-32 md:w-40 md:h-40 rounded-full overflow-hidden border border-slate-200 shadow-xl bg-white p-1 group"> <div className="relative w-32 h-32 md:w-40 md:h-40 rounded-full overflow-hidden border border-slate-200 shadow-xl bg-white p-1 group">
<div className="w-full h-full rounded-full overflow-hidden relative aspect-square"> <div className="w-full h-full rounded-full overflow-hidden relative aspect-square">
<img <img
src={getImgproxyUrl("/marc-mintel.png", { src="/marc-mintel.png"
width: 400,
height: 400,
resizing_type: "fill",
gravity: "sm",
})}
alt="Marc Mintel" alt="Marc Mintel"
className="object-cover grayscale transition-all duration-1000 ease-in-out scale-110 group-hover:scale-100 group-hover:grayscale-0 w-full h-full" className="object-cover grayscale transition-all duration-1000 ease-in-out scale-110 group-hover:scale-100 group-hover:grayscale-0 w-full h-full"
/> />

View File

@@ -1,6 +1,8 @@
import * as React from "react"; import * as React from "react";
import type { Metadata } from "next"; import type { Metadata } from "next";
import { notFound } from "next/navigation"; import { notFound, redirect } from "next/navigation";
import { getPayloadHMR } from "@payloadcms/next/utilities";
import configPromise from "@payload-config";
import { getAllPosts } from "@/src/lib/posts"; import { getAllPosts } from "@/src/lib/posts";
import { BlogPostHeader } from "@/src/components/blog/BlogPostHeader"; import { BlogPostHeader } from "@/src/components/blog/BlogPostHeader";
import { Section } from "@/src/components/Section"; import { Section } from "@/src/components/Section";
@@ -9,6 +11,8 @@ import { BlogPostClient } from "@/src/components/BlogPostClient";
import { TextSelectionShare } from "@/src/components/TextSelectionShare"; import { TextSelectionShare } from "@/src/components/TextSelectionShare";
import { BlogPostStickyBar } from "@/src/components/blog/BlogPostStickyBar"; import { BlogPostStickyBar } from "@/src/components/blog/BlogPostStickyBar";
import { MDXContent } from "@/src/components/MDXContent"; import { MDXContent } from "@/src/components/MDXContent";
import { PayloadRichText } from "@/src/components/PayloadRichText";
import { TableOfContents } from "@/src/components/TableOfContents";
export async function generateStaticParams() { export async function generateStaticParams() {
const allPosts = await getAllPosts(); const allPosts = await getAllPosts();
@@ -54,6 +58,18 @@ export default async function BlogPostPage({
const post = allPosts.find((p) => p.slug === slug); const post = allPosts.find((p) => p.slug === slug);
if (!post) { if (!post) {
const payload = await getPayloadHMR({ config: configPromise });
const redirectDoc = await payload.find({
collection: "redirects",
where: {
from: { equals: slug },
},
});
if (redirectDoc.docs.length > 0) {
redirect(`/blog/${redirectDoc.docs[0].to}`);
}
notFound(); notFound();
} }
@@ -102,7 +118,12 @@ export default async function BlogPostPage({
)} )}
<div className="article-content max-w-none"> <div className="article-content max-w-none">
<MDXContent code={post.body.code} /> <TableOfContents />
{post.lexicalContent ? (
<PayloadRichText data={post.lexicalContent} />
) : (
<MDXContent code={post.body.code} />
)}
</div> </div>
</Reveal> </Reveal>
</div> </div>

12
apps/web/check-db.ts Normal file
View File

@@ -0,0 +1,12 @@
export const run = async ({ payload }) => {
const docs = await payload.find({
collection: "context-files",
limit: 100,
});
console.log(`--- DB CHECK ---`);
console.log(`Found ${docs.totalDocs} context files.`);
docs.docs.forEach((doc) => {
console.log(`- ${doc.filename}`);
});
process.exit(0);
};

View File

@@ -1,64 +0,0 @@
# Marc — digital problem solver
## Identity
- Name: Marc Mintel
- Mail: marc@mintel.me
- Location: Vulkaneifel, Germany
- Role: Independent digital problem solver
- Mode: Solo
- Focus: Understanding problems and building practical solutions
## What I do
I work on digital problems and build tools, scripts, and systems to solve them.
Sometimes that means code, sometimes automation, sometimes AI, sometimes something else.
The tool is secondary. The problem comes first.
## How I work
- I try things
- I break things
- I fix things
- I write down what I learned
## What this blog is
A public notebook of:
- things I figured out
- mistakes I made
- tools I tested
- small insights that might be useful later
Mostly short entries.
Mostly practical.
## Why no portfolio
Finished projects get outdated.
Understanding doesnt.
This blog shows how I approach problems, not how pretty something looked last year.
## Topics
- Vibe coding with AI
- Debugging and problem solving
- Mac tools and workflows
- Automation
- Small scripts and systems
- Learning notes
- FOSS
## Audience
People who:
- build things
- work with computers
- solve problems
- and dont need marketing talk
## Tone
- calm
- factual
- direct
- no hype
- no self-promotion
## Core idea
Write things down.
So I dont forget.
And so others might find them useful.

View File

@@ -1,43 +0,0 @@
Prinzipien
Ich arbeite nach klaren Grundsätzen, die sicherstellen, dass meine Kunden fair, transparent und langfristig profitieren.
1. Volle Preis-Transparenz
Alle Kosten sind offen und nachvollziehbar.
Es gibt keine versteckten Gebühren, keine Abos, keine Lock-ins.
Jeder Kunde sieht genau, wofür er bezahlt.
2. Quellcode & Projektzugang
Auf Wunsch erhalten Kunden jederzeit den vollständigen Source Code und eine nachvollziehbare Struktur.
Damit kann jeder andere Entwickler problemlos weiterarbeiten.
Niemand kann später behaupten, der Code sei „Messy“ oder unbrauchbar.
3. Best Practices & saubere Technik
Ich setze konsequent bewährte Standards und dokumentierte Abläufe ein.
Das sorgt dafür, dass Systeme wartbar, verständlich und erweiterbar bleiben langfristig.
4. Verantwortung & Fairness
Ich übernehme die technische Verantwortung für die Website.
Ich garantiere keine Umsätze, Rankings oder rechtliche Ergebnisse nur saubere Umsetzung und stabile Systeme.
Wenn etwas nicht sinnvoll ist, sage ich es ehrlich.
5. Langfristiger Wert
Eine Website ist ein Investment.
Ich baue sie so, dass Anpassungen, Erweiterungen und Übergaben an andere Entwickler problemlos möglich sind.
Das schützt Ihre Investition und vermeidet teure Neuaufbauten.
6. Zusammenarbeit ohne Tricks
Keine künstlichen Deadlines, kein unnötiger Overhead.
Kommunikation ist klar, Entscheidungen nachvollziehbar, Übergaben sauber dokumentiert.

54
apps/web/migrate-docs.ts Normal file
View File

@@ -0,0 +1,54 @@
import { getPayload } from "payload";
import configPromise from "./payload.config";
import fs from "fs";
import path from "path";
async function run() {
try {
const payload = await getPayload({ config: configPromise });
console.log("Payload initialized.");
const docsDir = path.resolve(process.cwd(), "docs");
if (!fs.existsSync(docsDir)) {
console.log(`Docs directory not found at ${docsDir}`);
process.exit(0);
}
const files = fs.readdirSync(docsDir);
let count = 0;
for (const file of files) {
if (file.endsWith(".md")) {
const content = fs.readFileSync(path.join(docsDir, file), "utf8");
// Check if already exists
const existing = await payload.find({
collection: "context-files",
where: { filename: { equals: file } },
});
if (existing.totalDocs === 0) {
await payload.create({
collection: "context-files",
data: {
filename: file,
content: content,
},
});
count++;
}
}
}
console.log(
`Migration successful! Added ${count} new context files to the database.`,
);
process.exit(0);
} catch (e) {
console.error("Migration failed:", e);
process.exit(1);
}
}
run();

View File

@@ -0,0 +1,34 @@
import { getPayload } from "payload";
import configPromise from "./payload.config";
async function run() {
const payload = await getPayload({ config: configPromise });
const { docs } = await payload.find({
collection: "posts",
limit: 1000,
});
console.log(`Found ${docs.length} posts. Checking status...`);
for (const doc of docs) {
if (doc._status !== "published") {
try {
await payload.update({
collection: "posts",
id: doc.id,
data: {
_status: "published",
},
});
console.log(`Updated "${doc.title}" to published.`);
} catch (e) {
console.error(`Failed to update ${doc.title}:`, e.message);
}
}
}
console.log("Migration complete.");
process.exit(0);
}
run();

View File

@@ -1,17 +1,15 @@
import withMintelConfig from "@mintel/next-config"; import withMintelConfig from "@mintel/next-config";
import { withPayload } from '@payloadcms/next/withPayload'; import { withPayload } from '@payloadcms/next/withPayload';
import createMDX from '@next/mdx'; import createMDX from '@next/mdx';
import path from 'path';
import { fileURLToPath } from 'url';
const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename);
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'], serverExternalPackages: ['@mintel/content-engine'],
reactStrictMode: true,
output: 'standalone',
images: {
loader: 'custom',
loaderFile: './src/utils/imgproxy-loader.ts',
},
async rewrites() { async rewrites() {
return [ return [
// Umami proxy rewrite handled in app/stats/api/send/route.ts // Umami proxy rewrite handled in app/stats/api/send/route.ts
@@ -27,6 +25,13 @@ const nextConfig = {
}, },
]; ];
}, },
webpack: (config) => {
config.resolve.alias = {
...config.resolve.alias,
'@mintel/content-engine': path.resolve(dirname, 'node_modules/@mintel/content-engine'),
};
return config;
},
}; };
const withMDX = createMDX({ const withMDX = createMDX({

View File

@@ -4,7 +4,8 @@
"version": "0.1.0", "version": "0.1.0",
"description": "Technical problem solver's blog - practical insights and learning notes", "description": "Technical problem solver's blog - practical insights and learning notes",
"scripts": { "scripts": {
"dev": "rm -rf .next && next dev", "dev": "pnpm run seed:context && next dev --turbo",
"seed:context": "tsx ./seed-context.ts",
"build": "next build --webpack", "build": "next build --webpack",
"start": "next start", "start": "next start",
"lint": "eslint app src scripts video", "lint": "eslint app src scripts video",
@@ -40,6 +41,7 @@
"@payloadcms/email-nodemailer": "^3.77.0", "@payloadcms/email-nodemailer": "^3.77.0",
"@payloadcms/next": "^3.77.0", "@payloadcms/next": "^3.77.0",
"@payloadcms/richtext-lexical": "^3.77.0", "@payloadcms/richtext-lexical": "^3.77.0",
"@payloadcms/ui": "^3.77.0",
"@react-pdf/renderer": "^4.3.2", "@react-pdf/renderer": "^4.3.2",
"@remotion/bundler": "^4.0.414", "@remotion/bundler": "^4.0.414",
"@remotion/cli": "^4.0.414", "@remotion/cli": "^4.0.414",

View File

@@ -70,6 +70,9 @@ export interface Config {
users: User; users: User;
media: Media; media: Media;
posts: Post; posts: Post;
inquiries: Inquiry;
redirects: Redirect;
"context-files": ContextFile;
"payload-kv": PayloadKv; "payload-kv": PayloadKv;
"payload-locked-documents": PayloadLockedDocument; "payload-locked-documents": PayloadLockedDocument;
"payload-preferences": PayloadPreference; "payload-preferences": PayloadPreference;
@@ -80,6 +83,9 @@ export interface Config {
users: UsersSelect<false> | UsersSelect<true>; users: UsersSelect<false> | UsersSelect<true>;
media: MediaSelect<false> | MediaSelect<true>; media: MediaSelect<false> | MediaSelect<true>;
posts: PostsSelect<false> | PostsSelect<true>; posts: PostsSelect<false> | PostsSelect<true>;
inquiries: InquiriesSelect<false> | InquiriesSelect<true>;
redirects: RedirectsSelect<false> | RedirectsSelect<true>;
"context-files": ContextFilesSelect<false> | ContextFilesSelect<true>;
"payload-kv": PayloadKvSelect<false> | PayloadKvSelect<true>; "payload-kv": PayloadKvSelect<false> | PayloadKvSelect<true>;
"payload-locked-documents": "payload-locked-documents":
| PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<false>
@@ -95,8 +101,12 @@ export interface Config {
defaultIDType: number; defaultIDType: number;
}; };
fallbackLocale: null; fallbackLocale: null;
globals: {}; globals: {
globalsSelect: {}; "ai-settings": AiSetting;
};
globalsSelect: {
"ai-settings": AiSettingsSelect<false> | AiSettingsSelect<true>;
};
locale: null; locale: null;
user: User; user: User;
jobs: { jobs: {
@@ -201,12 +211,99 @@ export interface Post {
title: string; title: string;
slug: string; slug: string;
description: string; description: string;
/**
* Set a future date and save as 'Published' to schedule this post. It will not appear on the frontend until this date is reached.
*/
date: string; date: string;
tags: { tags: {
/**
* Kategorisiere diesen Post mit einem eindeutigen Tag
*/
tag?: string | null; tag?: string | null;
id?: string | null; id?: string | null;
}[]; }[];
thumbnail?: string | null; /**
* The main hero image for the blog post.
*/
featuredImage?: (number | null) | Media;
content?: {
root: {
type: string;
children: {
type: any;
version: number;
[k: string]: unknown;
}[];
direction: ("ltr" | "rtl") | null;
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
indent: number;
version: number;
};
[k: string]: unknown;
} | null;
updatedAt: string;
createdAt: string;
_status?: ("draft" | "published") | null;
}
/**
* Contact form leads and inquiries.
*
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "inquiries".
*/
export interface Inquiry {
id: number;
name: string;
email: string;
companyName?: string | null;
projectType?: string | null;
message?: string | null;
isFreeText?: boolean | null;
/**
* The JSON data from the configurator.
*/
config?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "redirects".
*/
export interface Redirect {
id: number;
/**
* The old URL slug that should be redirected (e.g. 'old-post-name')
*/
from: string;
/**
* The new URL slug to redirect to (e.g. 'new-awesome-post')
*/
to: string;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "context-files".
*/
export interface ContextFile {
id: number;
/**
* Exact filename (e.g. 'strategy.md'). The system uses this to identify the document during prompt generation.
*/
filename: string;
/**
* The raw markdown/text content of the document.
*/
content: string; content: string;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
@@ -246,6 +343,18 @@ export interface PayloadLockedDocument {
| ({ | ({
relationTo: "posts"; relationTo: "posts";
value: number | Post; value: number | Post;
} | null)
| ({
relationTo: "inquiries";
value: number | Inquiry;
} | null)
| ({
relationTo: "redirects";
value: number | Redirect;
} | null)
| ({
relationTo: "context-files";
value: number | ContextFile;
} | null); } | null);
globalSlug?: string | null; globalSlug?: string | null;
user: { user: {
@@ -378,7 +487,43 @@ export interface PostsSelect<T extends boolean = true> {
tag?: T; tag?: T;
id?: T; id?: T;
}; };
thumbnail?: T; featuredImage?: T;
content?: T;
updatedAt?: T;
createdAt?: T;
_status?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "inquiries_select".
*/
export interface InquiriesSelect<T extends boolean = true> {
name?: T;
email?: T;
companyName?: T;
projectType?: T;
message?: T;
isFreeText?: T;
config?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "redirects_select".
*/
export interface RedirectsSelect<T extends boolean = true> {
from?: T;
to?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "context-files_select".
*/
export interface ContextFilesSelect<T extends boolean = true> {
filename?: T;
content?: T; content?: T;
updatedAt?: T; updatedAt?: T;
createdAt?: T; createdAt?: T;
@@ -423,6 +568,39 @@ export interface PayloadMigrationsSelect<T extends boolean = true> {
updatedAt?: T; updatedAt?: T;
createdAt?: T; createdAt?: T;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "ai-settings".
*/
export interface AiSetting {
id: number;
/**
* List of trusted B2B/Tech sources (e.g. 'Vercel Blog', 'Fireship', 'Theo - t3.gg') the AI should prioritize when researching facts or videos. This overrides the hardcoded defaults.
*/
customSources?:
| {
sourceName: string;
id?: string | null;
}[]
| null;
updatedAt?: string | null;
createdAt?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "ai-settings_select".
*/
export interface AiSettingsSelect<T extends boolean = true> {
customSources?:
| T
| {
sourceName?: T;
id?: T;
};
updatedAt?: T;
createdAt?: T;
globalType?: T;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "auth". * via the `definition` "auth".

Some files were not shown because too many files have changed in this diff Show More