feat(cms): migrate from Directus to Payload v3 and remove contentlayer
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 3m43s
Build & Deploy / 🏗️ Build (push) Failing after 37s
Build & Deploy / 🧪 QA (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 2s
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 3m43s
Build & Deploy / 🏗️ Build (push) Failing after 37s
Build & Deploy / 🧪 QA (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 2s
This commit is contained in:
@@ -210,15 +210,12 @@ jobs:
|
|||||||
DIRECTUS_URL: ${{ needs.prepare.outputs.directus_url }}
|
DIRECTUS_URL: ${{ needs.prepare.outputs.directus_url }}
|
||||||
DIRECTUS_HOST: cms.${{ needs.prepare.outputs.traefik_host }}
|
DIRECTUS_HOST: cms.${{ needs.prepare.outputs.traefik_host }}
|
||||||
|
|
||||||
# Secrets mapping (Directus)
|
# Database configuration
|
||||||
DIRECTUS_KEY: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DIRECTUS_KEY) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_KEY) || secrets.DIRECTUS_KEY || vars.DIRECTUS_KEY }}
|
postgres_DB_NAME: ${{ secrets.DIRECTUS_DB_NAME || vars.DIRECTUS_DB_NAME || 'directus' }}
|
||||||
DIRECTUS_SECRET: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DIRECTUS_SECRET) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_SECRET) || secrets.DIRECTUS_SECRET || vars.DIRECTUS_SECRET }}
|
postgres_DB_USER: ${{ secrets.DIRECTUS_DB_USER || vars.DIRECTUS_DB_USER || 'directus' }}
|
||||||
DIRECTUS_ADMIN_EMAIL: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DIRECTUS_ADMIN_EMAIL) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_ADMIN_EMAIL) || secrets.DIRECTUS_ADMIN_EMAIL || vars.DIRECTUS_ADMIN_EMAIL || 'admin@mintel.me' }}
|
postgres_DB_PASSWORD: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DIRECTUS_DB_PASSWORD) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_DB_PASSWORD) || secrets.DIRECTUS_DB_PASSWORD || vars.DIRECTUS_DB_PASSWORD || 'directus' }}
|
||||||
DIRECTUS_ADMIN_PASSWORD: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DIRECTUS_ADMIN_PASSWORD) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_ADMIN_PASSWORD) || secrets.DIRECTUS_ADMIN_PASSWORD || vars.DIRECTUS_ADMIN_PASSWORD }}
|
DATABASE_URI: postgres://${{ env.postgres_DB_USER }}:${{ env.postgres_DB_PASSWORD }}@postgres-db:5432/${{ env.postgres_DB_NAME }}
|
||||||
DIRECTUS_DB_NAME: ${{ secrets.DIRECTUS_DB_NAME || vars.DIRECTUS_DB_NAME || 'directus' }}
|
PAYLOAD_SECRET: ${{ secrets.PAYLOAD_SECRET || vars.PAYLOAD_SECRET || 'secret' }}
|
||||||
DIRECTUS_DB_USER: ${{ secrets.DIRECTUS_DB_USER || vars.DIRECTUS_DB_USER || 'directus' }}
|
|
||||||
DIRECTUS_DB_PASSWORD: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DIRECTUS_DB_PASSWORD) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_DB_PASSWORD) || secrets.DIRECTUS_DB_PASSWORD || vars.DIRECTUS_DB_PASSWORD || 'directus' }}
|
|
||||||
DIRECTUS_API_TOKEN: ${{ (needs.prepare.outputs.target == 'testing' && secrets.TESTING_DIRECTUS_API_TOKEN) || (needs.prepare.outputs.target == 'staging' && secrets.STAGING_DIRECTUS_API_TOKEN) || secrets.DIRECTUS_API_TOKEN || vars.DIRECTUS_API_TOKEN }}
|
|
||||||
|
|
||||||
# Secrets mapping (Mail)
|
# Secrets mapping (Mail)
|
||||||
MAIL_HOST: ${{ secrets.SMTP_HOST || vars.SMTP_HOST }}
|
MAIL_HOST: ${{ secrets.SMTP_HOST || vars.SMTP_HOST }}
|
||||||
@@ -275,18 +272,12 @@ jobs:
|
|||||||
PROJECT_COLOR=$PROJECT_COLOR
|
PROJECT_COLOR=$PROJECT_COLOR
|
||||||
LOG_LEVEL=$LOG_LEVEL
|
LOG_LEVEL=$LOG_LEVEL
|
||||||
|
|
||||||
# Directus
|
# Payload DB
|
||||||
DIRECTUS_URL=$DIRECTUS_URL
|
postgres_DB_NAME=$postgres_DB_NAME
|
||||||
DIRECTUS_HOST=$DIRECTUS_HOST
|
postgres_DB_USER=$postgres_DB_USER
|
||||||
DIRECTUS_KEY=$DIRECTUS_KEY
|
postgres_DB_PASSWORD=$postgres_DB_PASSWORD
|
||||||
DIRECTUS_SECRET=$DIRECTUS_SECRET
|
DATABASE_URI=$DATABASE_URI
|
||||||
DIRECTUS_ADMIN_EMAIL=$DIRECTUS_ADMIN_EMAIL
|
PAYLOAD_SECRET=$PAYLOAD_SECRET
|
||||||
DIRECTUS_ADMIN_PASSWORD=$DIRECTUS_ADMIN_PASSWORD
|
|
||||||
DIRECTUS_DB_NAME=$DIRECTUS_DB_NAME
|
|
||||||
DIRECTUS_DB_USER=$DIRECTUS_DB_USER
|
|
||||||
DIRECTUS_DB_PASSWORD=$DIRECTUS_DB_PASSWORD
|
|
||||||
DIRECTUS_API_TOKEN=$DIRECTUS_API_TOKEN
|
|
||||||
INTERNAL_DIRECTUS_URL=http://directus:8055
|
|
||||||
|
|
||||||
# Mail
|
# Mail
|
||||||
MAIL_HOST=$MAIL_HOST
|
MAIL_HOST=$MAIL_HOST
|
||||||
@@ -337,8 +328,7 @@ jobs:
|
|||||||
ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' pull"
|
ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' pull"
|
||||||
ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' up -d --remove-orphans"
|
ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' up -d --remove-orphans"
|
||||||
|
|
||||||
# Apply Directus Schema Snapshot if available
|
|
||||||
ssh root@alpha.mintel.me "cd $SITE_DIR && if docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' exec -T directus ls /directus/schema/snapshot.yaml >/dev/null 2>&1; then echo '→ Applying Directus Schema Snapshot...' && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' exec -T directus npx directus schema apply /directus/schema/snapshot.yaml --yes; fi"
|
|
||||||
|
|
||||||
ssh root@alpha.mintel.me "docker system prune -f --filter 'until=24h'"
|
ssh root@alpha.mintel.me "docker system prune -f --filter 'until=24h'"
|
||||||
|
|
||||||
|
|||||||
25
apps/web/app/(payload)/admin/[[...segments]]/page.tsx
Normal file
25
apps/web/app/(payload)/admin/[[...segments]]/page.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
import configPromise from "@payload-config";
|
||||||
|
import { RootPage, generatePageMetadata } from "@payloadcms/next/views";
|
||||||
|
import { importMap } from "../importMap";
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
params: Promise<{
|
||||||
|
segments: string[];
|
||||||
|
}>;
|
||||||
|
searchParams: Promise<{
|
||||||
|
[key: string]: string | string[];
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateMetadata = async ({
|
||||||
|
params,
|
||||||
|
searchParams,
|
||||||
|
}: Args): Promise<Metadata> =>
|
||||||
|
generatePageMetadata({ config: configPromise, params, searchParams });
|
||||||
|
|
||||||
|
const Page = async ({ params, searchParams }: Args) =>
|
||||||
|
RootPage({ config: configPromise, importMap, params, searchParams });
|
||||||
|
|
||||||
|
export default Page;
|
||||||
75
apps/web/app/(payload)/admin/importMap.js
Normal file
75
apps/web/app/(payload)/admin/importMap.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
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 { 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 { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from "@payloadcms/next/rsc";
|
||||||
|
|
||||||
|
export const importMap = {
|
||||||
|
"@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#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,
|
||||||
|
"@payloadcms/next/rsc#CollectionCards":
|
||||||
|
CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1,
|
||||||
|
};
|
||||||
16
apps/web/app/(payload)/api/[...slug]/route.ts
Normal file
16
apps/web/app/(payload)/api/[...slug]/route.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import config from "@payload-config";
|
||||||
|
import {
|
||||||
|
REST_DELETE,
|
||||||
|
REST_GET,
|
||||||
|
REST_OPTIONS,
|
||||||
|
REST_PATCH,
|
||||||
|
REST_POST,
|
||||||
|
REST_PUT,
|
||||||
|
} from "@payloadcms/next/routes";
|
||||||
|
|
||||||
|
export const GET = REST_GET(config);
|
||||||
|
export const POST = REST_POST(config);
|
||||||
|
export const DELETE = REST_DELETE(config);
|
||||||
|
export const OPTIONS = REST_OPTIONS(config);
|
||||||
|
export const PATCH = REST_PATCH(config);
|
||||||
|
export const PUT = REST_PUT(config);
|
||||||
20
apps/web/app/(payload)/layout.tsx
Normal file
20
apps/web/app/(payload)/layout.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import configPromise from "@payload-config";
|
||||||
|
import "@payloadcms/next/css";
|
||||||
|
import { RootLayout } from "@payloadcms/next/layouts";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// @ts-expect-error - Export exists in JS but TS types are missing in this version
|
||||||
|
import { handleServerFunctions } from "@payloadcms/next/utilities";
|
||||||
|
import { importMap } from "./admin/importMap";
|
||||||
|
|
||||||
|
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<RootLayout
|
||||||
|
config={configPromise}
|
||||||
|
importMap={importMap}
|
||||||
|
serverFunction={handleServerFunctions}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</RootLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ImageResponse } from "next/og";
|
import { ImageResponse } from "next/og";
|
||||||
import { allPosts } from "contentlayer/generated";
|
import { getAllPosts } from "../../../src/lib/posts";
|
||||||
import { blogThumbnails } from "../../../src/components/blog/blogThumbnails";
|
import { blogThumbnails } from "../../../src/components/blog/blogThumbnails";
|
||||||
import { BlogOGImageTemplate } from "../../../src/components/BlogOGImageTemplate";
|
import { BlogOGImageTemplate } from "../../../src/components/BlogOGImageTemplate";
|
||||||
import { getOgFonts, OG_IMAGE_SIZE } from "../../../src/lib/og-helper";
|
import { getOgFonts, OG_IMAGE_SIZE } from "../../../src/lib/og-helper";
|
||||||
@@ -16,6 +16,7 @@ export default async function Image({
|
|||||||
params: Promise<{ slug: string }>;
|
params: Promise<{ slug: string }>;
|
||||||
}) {
|
}) {
|
||||||
const { slug } = await params;
|
const { slug } = await params;
|
||||||
|
const allPosts = await getAllPosts();
|
||||||
const post = allPosts.find((p) => p.slug === slug);
|
const post = allPosts.find((p) => p.slug === slug);
|
||||||
|
|
||||||
let backgroundImageSrc: string | undefined = undefined;
|
let backgroundImageSrc: string | undefined = undefined;
|
||||||
@@ -27,11 +28,15 @@ export default async function Image({
|
|||||||
const fileBuffer = await fs.readFile(filePath);
|
const fileBuffer = await fs.readFile(filePath);
|
||||||
|
|
||||||
const ext = path.extname(post.thumbnail).substring(1).toLowerCase();
|
const ext = path.extname(post.thumbnail).substring(1).toLowerCase();
|
||||||
const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
|
const mimeType =
|
||||||
|
ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
|
||||||
|
|
||||||
backgroundImageSrc = `data:${mimeType};base64,${fileBuffer.toString("base64")}`;
|
backgroundImageSrc = `data:${mimeType};base64,${fileBuffer.toString("base64")}`;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(`[OG Image Generator] Could not read thumbnail file for ${slug} to use as background:`, err);
|
console.warn(
|
||||||
|
`[OG Image Generator] Could not read thumbnail file for ${slug} to use as background:`,
|
||||||
|
err,
|
||||||
|
);
|
||||||
// Fall through to standard plain background
|
// Fall through to standard plain background
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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 } from "next/navigation";
|
||||||
import { allPosts } from "contentlayer/generated";
|
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";
|
||||||
import { Reveal } from "../../../src/components/Reveal";
|
import { Reveal } from "../../../src/components/Reveal";
|
||||||
@@ -11,6 +11,7 @@ import { BlogPostStickyBar } from "../../../src/components/blog/BlogPostStickyBa
|
|||||||
import { MDXContent } from "../../../src/components/MDXContent";
|
import { MDXContent } from "../../../src/components/MDXContent";
|
||||||
|
|
||||||
export async function generateStaticParams() {
|
export async function generateStaticParams() {
|
||||||
|
const allPosts = await getAllPosts();
|
||||||
return allPosts.map((post) => ({
|
return allPosts.map((post) => ({
|
||||||
slug: post.slug,
|
slug: post.slug,
|
||||||
}));
|
}));
|
||||||
@@ -22,6 +23,7 @@ export async function generateMetadata({
|
|||||||
params: Promise<{ slug: string }>;
|
params: Promise<{ slug: string }>;
|
||||||
}): Promise<Metadata> {
|
}): Promise<Metadata> {
|
||||||
const { slug } = await params;
|
const { slug } = await params;
|
||||||
|
const allPosts = await getAllPosts();
|
||||||
const post = allPosts.find((p) => p.slug === slug);
|
const post = allPosts.find((p) => p.slug === slug);
|
||||||
|
|
||||||
if (!post) return {};
|
if (!post) return {};
|
||||||
@@ -48,6 +50,7 @@ export default async function BlogPostPage({
|
|||||||
params: Promise<{ slug: string }>;
|
params: Promise<{ slug: string }>;
|
||||||
}) {
|
}) {
|
||||||
const { slug } = await params;
|
const { slug } = await params;
|
||||||
|
const allPosts = await getAllPosts();
|
||||||
const post = allPosts.find((p) => p.slug === slug);
|
const post = allPosts.find((p) => p.slug === slug);
|
||||||
|
|
||||||
if (!post) {
|
if (!post) {
|
||||||
|
|||||||
@@ -1,211 +1,14 @@
|
|||||||
"use client";
|
import { getAllPosts } from "../../src/lib/posts";
|
||||||
|
import { BlogClient } from "../../src/components/blog/BlogClient";
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
import * as React from "react";
|
export const metadata: Metadata = {
|
||||||
import { useState, useEffect } from "react";
|
title: "Blog | Mintel.me",
|
||||||
import { MediumCard } from "../../src/components/MediumCard";
|
description:
|
||||||
import { BlogCommandBar } from "../../src/components/blog/BlogCommandBar";
|
"Gedanken über Engineering, Design und die Architektur der Zukunft.",
|
||||||
import { allPosts as contentPosts } from "contentlayer/generated";
|
};
|
||||||
import { SectionHeader } from "../../src/components/SectionHeader";
|
|
||||||
import { Reveal } from "../../src/components/Reveal";
|
|
||||||
import { Section } from "../../src/components/Section";
|
|
||||||
import { AbstractCircuit, GradientMesh } from "../../src/components/Effects";
|
|
||||||
import { useAnalytics } from "../../src/components/analytics/useAnalytics";
|
|
||||||
|
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
export default async function BlogPage() {
|
||||||
|
const posts = await getAllPosts();
|
||||||
export default function BlogPage() {
|
return <BlogClient allPosts={posts as any} />;
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
|
||||||
const [activeTags, setActiveTags] = useState<string[]>([]);
|
|
||||||
const { trackEvent } = useAnalytics();
|
|
||||||
|
|
||||||
// Memoize allPosts
|
|
||||||
const allPosts = React.useMemo(
|
|
||||||
() =>
|
|
||||||
[...contentPosts].sort(
|
|
||||||
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(),
|
|
||||||
),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const [filteredPosts, setFilteredPosts] = useState(allPosts);
|
|
||||||
|
|
||||||
// Memoize allTags
|
|
||||||
const allTags = React.useMemo(
|
|
||||||
() => Array.from(new Set(allPosts.flatMap((post) => post.tags || []))),
|
|
||||||
[allPosts],
|
|
||||||
);
|
|
||||||
|
|
||||||
const [visibleCount, setVisibleCount] = useState(8);
|
|
||||||
|
|
||||||
const handleTagToggle = (tag: string) => {
|
|
||||||
setActiveTags((prev) =>
|
|
||||||
prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag],
|
|
||||||
);
|
|
||||||
setVisibleCount(8); // Reset pagination on filter change
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const query = searchQuery.toLowerCase().trim();
|
|
||||||
|
|
||||||
let filtered = allPosts;
|
|
||||||
|
|
||||||
if (query) {
|
|
||||||
filtered = filtered.filter((post) => {
|
|
||||||
const title = post.title.toLowerCase();
|
|
||||||
const description = post.description.toLowerCase();
|
|
||||||
const pTagString = (post.tags || []).join(" ").toLowerCase();
|
|
||||||
return (
|
|
||||||
title.includes(query) ||
|
|
||||||
description.includes(query) ||
|
|
||||||
pTagString.includes(query)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeTags.length > 0) {
|
|
||||||
filtered = filtered.filter((post) =>
|
|
||||||
post.tags?.some((tag) => activeTags.includes(tag)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
setFilteredPosts(filtered);
|
|
||||||
}, [searchQuery, activeTags, allPosts]);
|
|
||||||
|
|
||||||
const loadMore = () => {
|
|
||||||
setVisibleCount((prev) => prev + 6);
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasMore = visibleCount < filteredPosts.length;
|
|
||||||
const postsToShow = filteredPosts.slice(0, visibleCount);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col bg-slate-50/30 overflow-hidden relative min-h-screen">
|
|
||||||
<AbstractCircuit />
|
|
||||||
|
|
||||||
{/* Header Section */}
|
|
||||||
<header className="relative pt-32 pb-8 md:pt-44 md:pb-12 z-20 overflow-hidden">
|
|
||||||
<GradientMesh
|
|
||||||
variant="metallic"
|
|
||||||
className="opacity-20 absolute inset-0 -z-10"
|
|
||||||
/>
|
|
||||||
<div className="narrow-container">
|
|
||||||
<div className="space-y-4 text-center">
|
|
||||||
<Reveal direction="down" delay={0.1}>
|
|
||||||
<div className="flex items-center justify-center gap-4 text-[10px] font-bold uppercase tracking-[0.3em] text-slate-400">
|
|
||||||
<div className="w-6 h-px bg-slate-200" />
|
|
||||||
<span>Knowledge Base</span>
|
|
||||||
<div className="w-12 h-px bg-slate-100" />
|
|
||||||
</div>
|
|
||||||
</Reveal>
|
|
||||||
<Reveal delay={0.2}>
|
|
||||||
<div className="flex flex-col items-center gap-6">
|
|
||||||
<h1 className="text-5xl md:text-7xl font-bold text-slate-900 tracking-tighter leading-none">
|
|
||||||
Alle Artikel<span className="text-slate-300">.</span>
|
|
||||||
</h1>
|
|
||||||
<p className="font-serif italic text-slate-400 text-sm md:text-xl max-w-sm">
|
|
||||||
Gedanken über Engineering, Design und die Architektur der
|
|
||||||
Zukunft.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Reveal>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* Sticky Filter Bar */}
|
|
||||||
<div className="sticky top-0 z-40 bg-slate-50/80 backdrop-blur-xl border-y border-slate-200/50 py-4 shadow-sm transition-all duration-300">
|
|
||||||
<div className="narrow-container">
|
|
||||||
<Reveal width="100%">
|
|
||||||
<BlogCommandBar
|
|
||||||
searchQuery={searchQuery}
|
|
||||||
onSearchChange={setSearchQuery}
|
|
||||||
tags={allTags}
|
|
||||||
activeTags={activeTags}
|
|
||||||
onTagToggle={handleTagToggle}
|
|
||||||
/>
|
|
||||||
</Reveal>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Section
|
|
||||||
className="pb-32 pt-12"
|
|
||||||
containerVariant="narrow"
|
|
||||||
variant="white"
|
|
||||||
>
|
|
||||||
<div className="space-y-12 relative z-10 p-4 md:p-0">
|
|
||||||
{/* Posts List (Vertical & Minimal) */}
|
|
||||||
<div id="posts-container" className="space-y-12">
|
|
||||||
<AnimatePresence mode="popLayout" initial={false}>
|
|
||||||
{postsToShow.length === 0 ? (
|
|
||||||
<motion.div
|
|
||||||
key="no-results"
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.95 }}
|
|
||||||
className="py-24 text-center border border-dashed border-slate-200 rounded-3xl bg-white/50"
|
|
||||||
>
|
|
||||||
<p className="text-slate-400 font-mono text-sm uppercase tracking-widest">
|
|
||||||
Keine Beiträge gefunden.
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setSearchQuery("");
|
|
||||||
setActiveTags([]);
|
|
||||||
}}
|
|
||||||
className="mt-4 text-xs font-bold text-slate-900 underline underline-offset-4 hover:text-slate-600"
|
|
||||||
>
|
|
||||||
Filter zurücksetzen
|
|
||||||
</button>
|
|
||||||
</motion.div>
|
|
||||||
) : (
|
|
||||||
<motion.div
|
|
||||||
key="post-grid"
|
|
||||||
layout
|
|
||||||
className="grid grid-cols-1 gap-6 w-full"
|
|
||||||
>
|
|
||||||
<AnimatePresence mode="popLayout">
|
|
||||||
{postsToShow.map((post, i) => (
|
|
||||||
<motion.div
|
|
||||||
key={post.slug}
|
|
||||||
layout
|
|
||||||
initial={{ opacity: 0, scale: 0.9 }}
|
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.9 }}
|
|
||||||
transition={{
|
|
||||||
duration: 0.4,
|
|
||||||
delay: i < 8 ? i * 0.05 : 0,
|
|
||||||
ease: [0.16, 1, 0.3, 1]
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MediumCard post={post} />
|
|
||||||
</motion.div>
|
|
||||||
))}
|
|
||||||
</AnimatePresence>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
{/* Pagination */}
|
|
||||||
{hasMore && (
|
|
||||||
<div className="flex justify-center pt-8">
|
|
||||||
<Reveal delay={0.1} width="fit-content">
|
|
||||||
<button
|
|
||||||
onClick={loadMore}
|
|
||||||
className="group relative px-8 py-4 bg-white border border-slate-200 text-slate-600 rounded-full overflow-hidden transition-all hover:border-slate-400 hover:text-slate-900 hover:shadow-lg"
|
|
||||||
>
|
|
||||||
<span className="relative z-10 font-mono text-xs uppercase tracking-widest flex items-center gap-3">
|
|
||||||
Mehr laden
|
|
||||||
<div className="w-1 h-1 bg-slate-300 rounded-full group-hover:bg-slate-900 transition-colors" />
|
|
||||||
<div className="w-1 h-1 bg-slate-300 rounded-full group-hover:bg-slate-900 transition-colors delay-75" />
|
|
||||||
<div className="w-1 h-1 bg-slate-300 rounded-full group-hover:bg-slate-900 transition-colors delay-150" />
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</Reveal>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Section>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { MetadataRoute } from 'next';
|
import { MetadataRoute } from "next";
|
||||||
import { allPosts } from 'contentlayer/generated';
|
import { getAllPosts } from "../src/lib/posts";
|
||||||
import { technologies } from './technologies/[slug]/data';
|
import { technologies } from "./technologies/[slug]/data";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sitemap Generator
|
* Sitemap Generator
|
||||||
@@ -8,49 +8,50 @@ import { technologies } from './technologies/[slug]/data';
|
|||||||
* Standard Next.js 15 App Router sitemap generation.
|
* Standard Next.js 15 App Router sitemap generation.
|
||||||
* This file dynamically generates /sitemap.xml
|
* This file dynamically generates /sitemap.xml
|
||||||
*/
|
*/
|
||||||
export default function sitemap(): MetadataRoute.Sitemap {
|
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||||
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://mintel.me';
|
const allPosts = await getAllPosts();
|
||||||
|
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "https://mintel.me";
|
||||||
|
|
||||||
// 1. Core Pages
|
// 1. Core Pages
|
||||||
const routes = [
|
const routes = [
|
||||||
'',
|
"",
|
||||||
'/about',
|
"/about",
|
||||||
'/blog',
|
"/blog",
|
||||||
'/case-studies',
|
"/case-studies",
|
||||||
'/case-studies/klz-cables',
|
"/case-studies/klz-cables",
|
||||||
'/contact',
|
"/contact",
|
||||||
'/websites',
|
"/websites",
|
||||||
].map((route) => ({
|
].map((route) => ({
|
||||||
url: `${baseUrl}${route}`,
|
url: `${baseUrl}${route}`,
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
changeFrequency: 'monthly' as const,
|
changeFrequency: "monthly" as const,
|
||||||
priority: route === '' ? 1.0 : 0.8,
|
priority: route === "" ? 1.0 : 0.8,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 2. Dynamic Blog Posts
|
// 2. Dynamic Blog Posts
|
||||||
const blogRoutes = allPosts.map((post) => ({
|
const blogRoutes = allPosts.map((post) => ({
|
||||||
url: `${baseUrl}/blog/${post.slug}`,
|
url: `${baseUrl}/blog/${post.slug}`,
|
||||||
lastModified: new Date(post.date),
|
lastModified: new Date(post.date),
|
||||||
changeFrequency: 'monthly' as const,
|
changeFrequency: "monthly" as const,
|
||||||
priority: 0.7,
|
priority: 0.7,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 3. Technology Detail Pages
|
// 3. Technology Detail Pages
|
||||||
const techRoutes = Object.keys(technologies).map((slug) => ({
|
const techRoutes = Object.keys(technologies).map((slug) => ({
|
||||||
url: `${baseUrl}/technologies/${slug}`,
|
url: `${baseUrl}/technologies/${slug}`,
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
changeFrequency: 'monthly' as const,
|
changeFrequency: "monthly" as const,
|
||||||
priority: 0.6,
|
priority: 0.6,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 4. Tag Pages
|
// 4. Tag Pages
|
||||||
const allTags = [...new Set(allPosts.flatMap((post) => post.tags))];
|
const allTags = [...new Set(allPosts.flatMap((post) => post.tags))];
|
||||||
const tagRoutes = allTags.map((tag) => ({
|
const tagRoutes = allTags.map((tag) => ({
|
||||||
url: `${baseUrl}/tags/${tag}`,
|
url: `${baseUrl}/tags/${tag}`,
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
changeFrequency: 'weekly' as const,
|
changeFrequency: "weekly" as const,
|
||||||
priority: 0.3,
|
priority: 0.3,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return [...routes, ...blogRoutes, ...techRoutes, ...tagRoutes];
|
return [...routes, ...blogRoutes, ...techRoutes, ...tagRoutes];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { allPosts } from "contentlayer/generated";
|
import { getAllPosts } from "../../../src/lib/posts";
|
||||||
import { MediumCard } from "../../../src/components/MediumCard";
|
import { MediumCard } from "../../../src/components/MediumCard";
|
||||||
import { Reveal } from "../../../src/components/Reveal";
|
import { Reveal } from "../../../src/components/Reveal";
|
||||||
|
|
||||||
export async function generateStaticParams() {
|
export async function generateStaticParams() {
|
||||||
|
const allPosts = await getAllPosts();
|
||||||
const allTags = Array.from(
|
const allTags = Array.from(
|
||||||
new Set(allPosts.flatMap((post) => post.tags || [])),
|
new Set(allPosts.flatMap((post) => post.tags || [])),
|
||||||
);
|
);
|
||||||
@@ -18,6 +19,7 @@ export default async function TagPage({
|
|||||||
params: Promise<{ tag: string }>;
|
params: Promise<{ tag: string }>;
|
||||||
}) {
|
}) {
|
||||||
const { tag } = await params;
|
const { tag } = await params;
|
||||||
|
const allPosts = await getAllPosts();
|
||||||
const posts = allPosts.filter((post) => post.tags?.includes(tag));
|
const posts = allPosts.filter((post) => post.tags?.includes(tag));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
import { defineDocumentType, makeSource } from 'contentlayer2/source-files'
|
|
||||||
|
|
||||||
export const Post = defineDocumentType(() => ({
|
|
||||||
name: 'Post',
|
|
||||||
filePathPattern: `blog/**/*.mdx`,
|
|
||||||
contentType: 'mdx',
|
|
||||||
fields: {
|
|
||||||
title: { type: 'string', required: true },
|
|
||||||
date: { type: 'string', required: true },
|
|
||||||
description: { type: 'string', required: true },
|
|
||||||
tags: { type: 'list', of: { type: 'string' }, required: true },
|
|
||||||
thumbnail: { type: 'string', required: false },
|
|
||||||
},
|
|
||||||
computedFields: {
|
|
||||||
slug: {
|
|
||||||
type: 'string',
|
|
||||||
resolve: (doc) => doc._raw.sourceFileName.replace(/\.mdx$/, ''),
|
|
||||||
},
|
|
||||||
url: {
|
|
||||||
type: 'string',
|
|
||||||
resolve: (post) => `/blog/${post._raw.sourceFileName.replace(/\.mdx$/, '')}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
export default makeSource({
|
|
||||||
contentDirPath: 'content',
|
|
||||||
documentTypes: [Post],
|
|
||||||
})
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { withContentlayer } from 'next-contentlayer2';
|
|
||||||
import withMintelConfig from "@mintel/next-config";
|
import withMintelConfig from "@mintel/next-config";
|
||||||
|
import { withPayload } from '@payloadcms/next/withPayload';
|
||||||
|
|
||||||
import createMDX from '@next/mdx';
|
import createMDX from '@next/mdx';
|
||||||
|
|
||||||
@@ -32,5 +32,4 @@ const nextConfig = {
|
|||||||
const withMDX = createMDX({
|
const withMDX = createMDX({
|
||||||
// Add markdown plugins here, as desired
|
// Add markdown plugins here, as desired
|
||||||
});
|
});
|
||||||
|
export default withPayload(withMintelConfig(withMDX(nextConfig)));
|
||||||
export default withContentlayer(withMintelConfig(withMDX(nextConfig)));
|
|
||||||
|
|||||||
@@ -19,17 +19,9 @@
|
|||||||
"video:render:button": "remotion render video/index.ts ButtonShowcase out/button-showcase.mp4 --concurrency=1 --codec=h264 --crf=16 --pixel-format=yuv420p --overwrite",
|
"video:render:button": "remotion render video/index.ts ButtonShowcase out/button-showcase.mp4 --concurrency=1 --codec=h264 --crf=16 --pixel-format=yuv420p --overwrite",
|
||||||
"video:render:all": "npm run video:render:contact && npm run video:render:button",
|
"video:render:all": "npm run video:render:contact && npm run video:render:button",
|
||||||
"pagespeed:test": "npx tsx ./scripts/pagespeed-sitemap.ts",
|
"pagespeed:test": "npx tsx ./scripts/pagespeed-sitemap.ts",
|
||||||
"cms:bootstrap": "DIRECTUS_URL=http://localhost:8055 npx tsx --env-file=.env scripts/setup-directus.ts",
|
|
||||||
"cms:push:staging": "../../scripts/sync-directus.sh push staging",
|
|
||||||
"cms:pull:staging": "../../scripts/sync-directus.sh pull staging",
|
|
||||||
"cms:push:testing": "../../scripts/sync-directus.sh push testing",
|
|
||||||
"cms:pull:testing": "../../scripts/sync-directus.sh pull testing",
|
|
||||||
"cms:push:prod": "../../scripts/sync-directus.sh push production",
|
|
||||||
"cms:pull:prod": "../../scripts/sync-directus.sh pull production",
|
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@directus/sdk": "21.0.0",
|
|
||||||
"@emotion/is-prop-valid": "^1.4.0",
|
"@emotion/is-prop-valid": "^1.4.0",
|
||||||
"@mdx-js/loader": "^3.1.1",
|
"@mdx-js/loader": "^3.1.1",
|
||||||
"@mdx-js/react": "^3.1.1",
|
"@mdx-js/react": "^3.1.1",
|
||||||
@@ -44,6 +36,9 @@
|
|||||||
"@opentelemetry/context-async-hooks": "^2.1.0",
|
"@opentelemetry/context-async-hooks": "^2.1.0",
|
||||||
"@opentelemetry/core": "^2.1.0",
|
"@opentelemetry/core": "^2.1.0",
|
||||||
"@opentelemetry/sdk-trace-base": "^2.1.0",
|
"@opentelemetry/sdk-trace-base": "^2.1.0",
|
||||||
|
"@payloadcms/db-postgres": "^3.77.0",
|
||||||
|
"@payloadcms/next": "^3.77.0",
|
||||||
|
"@payloadcms/richtext-lexical": "^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",
|
||||||
@@ -60,18 +55,18 @@
|
|||||||
"axios": "^1.13.4",
|
"axios": "^1.13.4",
|
||||||
"canvas-confetti": "^1.9.4",
|
"canvas-confetti": "^1.9.4",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"contentlayer2": "^0.5.8",
|
|
||||||
"crawlee": "^3.15.3",
|
"crawlee": "^3.15.3",
|
||||||
"esbuild": "^0.27.3",
|
"esbuild": "^0.27.3",
|
||||||
"framer-motion": "^12.29.2",
|
"framer-motion": "^12.29.2",
|
||||||
|
"graphql": "^16.12.0",
|
||||||
"html-to-image": "^1.11.13",
|
"html-to-image": "^1.11.13",
|
||||||
"ioredis": "^5.9.1",
|
"ioredis": "^5.9.1",
|
||||||
"lucide-react": "^0.468.0",
|
"lucide-react": "^0.468.0",
|
||||||
"mermaid": "^11.12.2",
|
"mermaid": "^11.12.2",
|
||||||
"next": "^16.1.6",
|
"next": "^16.1.6",
|
||||||
"next-contentlayer2": "^0.5.8",
|
|
||||||
"next-mdx-remote": "^6.0.0",
|
"next-mdx-remote": "^6.0.0",
|
||||||
"nodemailer": "^8.0.1",
|
"nodemailer": "^8.0.1",
|
||||||
|
"payload": "^3.77.0",
|
||||||
"playwright": "^1.58.1",
|
"playwright": "^1.58.1",
|
||||||
"prismjs": "^1.30.0",
|
"prismjs": "^1.30.0",
|
||||||
"puppeteer": "^24.36.1",
|
"puppeteer": "^24.36.1",
|
||||||
@@ -82,6 +77,7 @@
|
|||||||
"react-tweet": "^3.3.0",
|
"react-tweet": "^3.3.0",
|
||||||
"recharts": "^3.7.0",
|
"recharts": "^3.7.0",
|
||||||
"remotion": "^4.0.414",
|
"remotion": "^4.0.414",
|
||||||
|
"sharp": "^0.34.5",
|
||||||
"shiki": "^1.24.2",
|
"shiki": "^1.24.2",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss": "^3.4.0",
|
"tailwindcss": "^3.4.0",
|
||||||
|
|||||||
436
apps/web/payload-types.ts
Normal file
436
apps/web/payload-types.ts
Normal file
@@ -0,0 +1,436 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* This file was automatically generated by Payload.
|
||||||
|
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
||||||
|
* and re-run `payload generate:types` to regenerate this file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supported timezones in IANA format.
|
||||||
|
*
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "supportedTimezones".
|
||||||
|
*/
|
||||||
|
export type SupportedTimezones =
|
||||||
|
| "Pacific/Midway"
|
||||||
|
| "Pacific/Niue"
|
||||||
|
| "Pacific/Honolulu"
|
||||||
|
| "Pacific/Rarotonga"
|
||||||
|
| "America/Anchorage"
|
||||||
|
| "Pacific/Gambier"
|
||||||
|
| "America/Los_Angeles"
|
||||||
|
| "America/Tijuana"
|
||||||
|
| "America/Denver"
|
||||||
|
| "America/Phoenix"
|
||||||
|
| "America/Chicago"
|
||||||
|
| "America/Guatemala"
|
||||||
|
| "America/New_York"
|
||||||
|
| "America/Bogota"
|
||||||
|
| "America/Caracas"
|
||||||
|
| "America/Santiago"
|
||||||
|
| "America/Buenos_Aires"
|
||||||
|
| "America/Sao_Paulo"
|
||||||
|
| "Atlantic/South_Georgia"
|
||||||
|
| "Atlantic/Azores"
|
||||||
|
| "Atlantic/Cape_Verde"
|
||||||
|
| "Europe/London"
|
||||||
|
| "Europe/Berlin"
|
||||||
|
| "Africa/Lagos"
|
||||||
|
| "Europe/Athens"
|
||||||
|
| "Africa/Cairo"
|
||||||
|
| "Europe/Moscow"
|
||||||
|
| "Asia/Riyadh"
|
||||||
|
| "Asia/Dubai"
|
||||||
|
| "Asia/Baku"
|
||||||
|
| "Asia/Karachi"
|
||||||
|
| "Asia/Tashkent"
|
||||||
|
| "Asia/Calcutta"
|
||||||
|
| "Asia/Dhaka"
|
||||||
|
| "Asia/Almaty"
|
||||||
|
| "Asia/Jakarta"
|
||||||
|
| "Asia/Bangkok"
|
||||||
|
| "Asia/Shanghai"
|
||||||
|
| "Asia/Singapore"
|
||||||
|
| "Asia/Tokyo"
|
||||||
|
| "Asia/Seoul"
|
||||||
|
| "Australia/Brisbane"
|
||||||
|
| "Australia/Sydney"
|
||||||
|
| "Pacific/Guam"
|
||||||
|
| "Pacific/Noumea"
|
||||||
|
| "Pacific/Auckland"
|
||||||
|
| "Pacific/Fiji";
|
||||||
|
|
||||||
|
export interface Config {
|
||||||
|
auth: {
|
||||||
|
users: UserAuthOperations;
|
||||||
|
};
|
||||||
|
blocks: {};
|
||||||
|
collections: {
|
||||||
|
users: User;
|
||||||
|
media: Media;
|
||||||
|
posts: Post;
|
||||||
|
"payload-kv": PayloadKv;
|
||||||
|
"payload-locked-documents": PayloadLockedDocument;
|
||||||
|
"payload-preferences": PayloadPreference;
|
||||||
|
"payload-migrations": PayloadMigration;
|
||||||
|
};
|
||||||
|
collectionsJoins: {};
|
||||||
|
collectionsSelect: {
|
||||||
|
users: UsersSelect<false> | UsersSelect<true>;
|
||||||
|
media: MediaSelect<false> | MediaSelect<true>;
|
||||||
|
posts: PostsSelect<false> | PostsSelect<true>;
|
||||||
|
"payload-kv": PayloadKvSelect<false> | PayloadKvSelect<true>;
|
||||||
|
"payload-locked-documents":
|
||||||
|
| PayloadLockedDocumentsSelect<false>
|
||||||
|
| PayloadLockedDocumentsSelect<true>;
|
||||||
|
"payload-preferences":
|
||||||
|
| PayloadPreferencesSelect<false>
|
||||||
|
| PayloadPreferencesSelect<true>;
|
||||||
|
"payload-migrations":
|
||||||
|
| PayloadMigrationsSelect<false>
|
||||||
|
| PayloadMigrationsSelect<true>;
|
||||||
|
};
|
||||||
|
db: {
|
||||||
|
defaultIDType: number;
|
||||||
|
};
|
||||||
|
fallbackLocale: null;
|
||||||
|
globals: {};
|
||||||
|
globalsSelect: {};
|
||||||
|
locale: null;
|
||||||
|
user: User;
|
||||||
|
jobs: {
|
||||||
|
tasks: unknown;
|
||||||
|
workflows: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export interface UserAuthOperations {
|
||||||
|
forgotPassword: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
login: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
registerFirstUser: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
unlock: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "users".
|
||||||
|
*/
|
||||||
|
export interface User {
|
||||||
|
id: number;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
email: string;
|
||||||
|
resetPasswordToken?: string | null;
|
||||||
|
resetPasswordExpiration?: string | null;
|
||||||
|
salt?: string | null;
|
||||||
|
hash?: string | null;
|
||||||
|
loginAttempts?: number | null;
|
||||||
|
lockUntil?: string | null;
|
||||||
|
sessions?:
|
||||||
|
| {
|
||||||
|
id: string;
|
||||||
|
createdAt?: string | null;
|
||||||
|
expiresAt: string;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
password?: string | null;
|
||||||
|
collection: "users";
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "media".
|
||||||
|
*/
|
||||||
|
export interface Media {
|
||||||
|
id: number;
|
||||||
|
alt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
url?: string | null;
|
||||||
|
thumbnailURL?: string | null;
|
||||||
|
filename?: string | null;
|
||||||
|
mimeType?: string | null;
|
||||||
|
filesize?: number | null;
|
||||||
|
width?: number | null;
|
||||||
|
height?: number | null;
|
||||||
|
focalX?: number | null;
|
||||||
|
focalY?: number | null;
|
||||||
|
sizes?: {
|
||||||
|
thumbnail?: {
|
||||||
|
url?: string | null;
|
||||||
|
width?: number | null;
|
||||||
|
height?: number | null;
|
||||||
|
mimeType?: string | null;
|
||||||
|
filesize?: number | null;
|
||||||
|
filename?: string | null;
|
||||||
|
};
|
||||||
|
card?: {
|
||||||
|
url?: string | null;
|
||||||
|
width?: number | null;
|
||||||
|
height?: number | null;
|
||||||
|
mimeType?: string | null;
|
||||||
|
filesize?: number | null;
|
||||||
|
filename?: string | null;
|
||||||
|
};
|
||||||
|
tablet?: {
|
||||||
|
url?: string | null;
|
||||||
|
width?: number | null;
|
||||||
|
height?: number | null;
|
||||||
|
mimeType?: string | null;
|
||||||
|
filesize?: number | null;
|
||||||
|
filename?: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "posts".
|
||||||
|
*/
|
||||||
|
export interface Post {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
slug: string;
|
||||||
|
description: string;
|
||||||
|
date: string;
|
||||||
|
tags: {
|
||||||
|
tag?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[];
|
||||||
|
thumbnail?: string | null;
|
||||||
|
content: string;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-kv".
|
||||||
|
*/
|
||||||
|
export interface PayloadKv {
|
||||||
|
id: number;
|
||||||
|
key: string;
|
||||||
|
data:
|
||||||
|
| {
|
||||||
|
[k: string]: unknown;
|
||||||
|
}
|
||||||
|
| unknown[]
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-locked-documents".
|
||||||
|
*/
|
||||||
|
export interface PayloadLockedDocument {
|
||||||
|
id: number;
|
||||||
|
document?:
|
||||||
|
| ({
|
||||||
|
relationTo: "users";
|
||||||
|
value: number | User;
|
||||||
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: "media";
|
||||||
|
value: number | Media;
|
||||||
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: "posts";
|
||||||
|
value: number | Post;
|
||||||
|
} | null);
|
||||||
|
globalSlug?: string | null;
|
||||||
|
user: {
|
||||||
|
relationTo: "users";
|
||||||
|
value: number | User;
|
||||||
|
};
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-preferences".
|
||||||
|
*/
|
||||||
|
export interface PayloadPreference {
|
||||||
|
id: number;
|
||||||
|
user: {
|
||||||
|
relationTo: "users";
|
||||||
|
value: number | User;
|
||||||
|
};
|
||||||
|
key?: string | null;
|
||||||
|
value?:
|
||||||
|
| {
|
||||||
|
[k: string]: unknown;
|
||||||
|
}
|
||||||
|
| unknown[]
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-migrations".
|
||||||
|
*/
|
||||||
|
export interface PayloadMigration {
|
||||||
|
id: number;
|
||||||
|
name?: string | null;
|
||||||
|
batch?: number | null;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "users_select".
|
||||||
|
*/
|
||||||
|
export interface UsersSelect<T extends boolean = true> {
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
email?: T;
|
||||||
|
resetPasswordToken?: T;
|
||||||
|
resetPasswordExpiration?: T;
|
||||||
|
salt?: T;
|
||||||
|
hash?: T;
|
||||||
|
loginAttempts?: T;
|
||||||
|
lockUntil?: T;
|
||||||
|
sessions?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
id?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
expiresAt?: T;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "media_select".
|
||||||
|
*/
|
||||||
|
export interface MediaSelect<T extends boolean = true> {
|
||||||
|
alt?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
url?: T;
|
||||||
|
thumbnailURL?: T;
|
||||||
|
filename?: T;
|
||||||
|
mimeType?: T;
|
||||||
|
filesize?: T;
|
||||||
|
width?: T;
|
||||||
|
height?: T;
|
||||||
|
focalX?: T;
|
||||||
|
focalY?: T;
|
||||||
|
sizes?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
thumbnail?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
url?: T;
|
||||||
|
width?: T;
|
||||||
|
height?: T;
|
||||||
|
mimeType?: T;
|
||||||
|
filesize?: T;
|
||||||
|
filename?: T;
|
||||||
|
};
|
||||||
|
card?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
url?: T;
|
||||||
|
width?: T;
|
||||||
|
height?: T;
|
||||||
|
mimeType?: T;
|
||||||
|
filesize?: T;
|
||||||
|
filename?: T;
|
||||||
|
};
|
||||||
|
tablet?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
url?: T;
|
||||||
|
width?: T;
|
||||||
|
height?: T;
|
||||||
|
mimeType?: T;
|
||||||
|
filesize?: T;
|
||||||
|
filename?: T;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "posts_select".
|
||||||
|
*/
|
||||||
|
export interface PostsSelect<T extends boolean = true> {
|
||||||
|
title?: T;
|
||||||
|
slug?: T;
|
||||||
|
description?: T;
|
||||||
|
date?: T;
|
||||||
|
tags?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
tag?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
thumbnail?: T;
|
||||||
|
content?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-kv_select".
|
||||||
|
*/
|
||||||
|
export interface PayloadKvSelect<T extends boolean = true> {
|
||||||
|
key?: T;
|
||||||
|
data?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-locked-documents_select".
|
||||||
|
*/
|
||||||
|
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
|
||||||
|
document?: T;
|
||||||
|
globalSlug?: T;
|
||||||
|
user?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-preferences_select".
|
||||||
|
*/
|
||||||
|
export interface PayloadPreferencesSelect<T extends boolean = true> {
|
||||||
|
user?: T;
|
||||||
|
key?: T;
|
||||||
|
value?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-migrations_select".
|
||||||
|
*/
|
||||||
|
export interface PayloadMigrationsSelect<T extends boolean = true> {
|
||||||
|
name?: T;
|
||||||
|
batch?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "auth".
|
||||||
|
*/
|
||||||
|
export interface Auth {
|
||||||
|
[k: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "payload" {
|
||||||
|
export interface GeneratedTypes extends Config {}
|
||||||
|
}
|
||||||
36
apps/web/payload.config.ts
Normal file
36
apps/web/payload.config.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { buildConfig } from "payload";
|
||||||
|
import { postgresAdapter } from "@payloadcms/db-postgres";
|
||||||
|
import { lexicalEditor } from "@payloadcms/richtext-lexical";
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import sharp from "sharp";
|
||||||
|
|
||||||
|
import { Users } from "./src/payload/collections/Users";
|
||||||
|
import { Media } from "./src/payload/collections/Media";
|
||||||
|
import { Posts } from "./src/payload/collections/Posts";
|
||||||
|
|
||||||
|
const filename = fileURLToPath(import.meta.url);
|
||||||
|
const dirname = path.dirname(filename);
|
||||||
|
|
||||||
|
export default buildConfig({
|
||||||
|
admin: {
|
||||||
|
user: Users.slug,
|
||||||
|
importMap: {
|
||||||
|
baseDir: path.resolve(dirname),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
collections: [Users, Media, Posts],
|
||||||
|
editor: lexicalEditor(),
|
||||||
|
secret: process.env.PAYLOAD_SECRET || "fallback-secret-for-dev",
|
||||||
|
typescript: {
|
||||||
|
outputFile: path.resolve(dirname, "payload-types.ts"),
|
||||||
|
},
|
||||||
|
db: postgresAdapter({
|
||||||
|
pool: {
|
||||||
|
connectionString:
|
||||||
|
process.env.DATABASE_URI || process.env.POSTGRES_URI || "",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
sharp,
|
||||||
|
plugins: [],
|
||||||
|
});
|
||||||
72
apps/web/scripts/migrate-posts.ts
Normal file
72
apps/web/scripts/migrate-posts.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { getPayload } from "payload";
|
||||||
|
import configPromise from "../payload.config";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
function parseMatter(content: string) {
|
||||||
|
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
||||||
|
if (!match) return { data: {}, content };
|
||||||
|
const data: Record<string, any> = {};
|
||||||
|
match[1].split("\n").forEach((line) => {
|
||||||
|
const [key, ...rest] = line.split(":");
|
||||||
|
if (key && rest.length) {
|
||||||
|
const field = key.trim();
|
||||||
|
let val = rest.join(":").trim();
|
||||||
|
if (val.startsWith("[")) {
|
||||||
|
// basic array parsing
|
||||||
|
data[field] = val
|
||||||
|
.slice(1, -1)
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim().replace(/^["']|["']$/g, ""));
|
||||||
|
} else {
|
||||||
|
data[field] = val.replace(/^["']|["']$/g, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { data, content: match[2].trim() };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
const payload = await getPayload({ config: configPromise });
|
||||||
|
const contentDir = path.join(process.cwd(), "content", "blog");
|
||||||
|
const files = fs.readdirSync(contentDir).filter((f) => f.endsWith(".mdx"));
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = path.join(contentDir, file);
|
||||||
|
const content = fs.readFileSync(filePath, "utf-8");
|
||||||
|
const { data, content: body } = parseMatter(content);
|
||||||
|
|
||||||
|
const slug = file.replace(/\.mdx$/, "");
|
||||||
|
console.log(`Migrating ${slug}...`);
|
||||||
|
|
||||||
|
const existing = await payload.find({
|
||||||
|
collection: "posts",
|
||||||
|
where: { slug: { equals: slug } },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existing.docs.length === 0) {
|
||||||
|
await payload.create({
|
||||||
|
collection: "posts",
|
||||||
|
data: {
|
||||||
|
title: data.title || slug,
|
||||||
|
slug,
|
||||||
|
description: data.description || "",
|
||||||
|
date: data.date
|
||||||
|
? new Date(data.date).toISOString()
|
||||||
|
: new Date().toISOString(),
|
||||||
|
tags: (data.tags || []).map((t: string) => ({ tag: t })),
|
||||||
|
thumbnail: data.thumbnail || "",
|
||||||
|
content: body,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(`✔ Inserted ${slug}`);
|
||||||
|
} else {
|
||||||
|
console.log(`⚠ Skipped ${slug} (already exists)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Migration complete.");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch(console.error);
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import {
|
|
||||||
createMintelDirectusClient,
|
|
||||||
ensureDirectusAuthenticated,
|
|
||||||
} from "@mintel/next-utils";
|
|
||||||
import { updateSettings } from "@directus/sdk";
|
|
||||||
|
|
||||||
const client = createMintelDirectusClient();
|
|
||||||
|
|
||||||
async function setupBranding() {
|
|
||||||
const prjName = process.env.PROJECT_NAME || "Mintel.me";
|
|
||||||
const prjColor = process.env.PROJECT_COLOR || "#ff00ff";
|
|
||||||
|
|
||||||
console.log(`🎨 Refining Directus Branding for ${prjName}...`);
|
|
||||||
await ensureDirectusAuthenticated(client);
|
|
||||||
|
|
||||||
const cssInjection = `
|
|
||||||
<style>
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&display=swap');
|
|
||||||
|
|
||||||
body, .v-app { font-family: 'Outfit', sans-serif !important; }
|
|
||||||
|
|
||||||
.public-view .v-card {
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
background: rgba(255, 255, 255, 0.9) !important;
|
|
||||||
border-radius: 32px !important;
|
|
||||||
box-shadow: 0 50px 100px -20px rgba(0, 0, 0, 0.4) !important;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.3) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-navigation-drawer { background: #000c24 !important; }
|
|
||||||
|
|
||||||
.v-list-item--active {
|
|
||||||
color: ${prjColor} !important;
|
|
||||||
background: rgba(255, 0, 255, 0.1) !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<div style="font-family: 'Outfit', sans-serif; text-align: center; margin-top: 24px;">
|
|
||||||
<p style="color: rgba(255,255,255,0.6); font-size: 11px; letter-spacing: 2px; margin-bottom: 4px; font-weight: 600; text-transform: uppercase;">Mintel Infrastructure Engine</p>
|
|
||||||
<h1 style="color: #ffffff; font-size: 20px; font-weight: 700; margin: 0; letter-spacing: -0.5px;">${prjName.toUpperCase()} <span style="color: ${prjColor};">SYNC.</span></h1>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await client.request(
|
|
||||||
updateSettings({
|
|
||||||
project_name: prjName,
|
|
||||||
project_color: prjColor,
|
|
||||||
public_note: cssInjection,
|
|
||||||
module_bar_background: "#00081a",
|
|
||||||
theme_light_overrides: {
|
|
||||||
primary: prjColor,
|
|
||||||
borderRadius: "12px",
|
|
||||||
navigationBackground: "#000c24",
|
|
||||||
navigationForeground: "#ffffff",
|
|
||||||
moduleBarBackground: "#00081a",
|
|
||||||
},
|
|
||||||
} as any),
|
|
||||||
);
|
|
||||||
console.log("✨ Branding applied!");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Error during bootstrap:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setupBranding()
|
|
||||||
.then(() => {
|
|
||||||
process.exit(0);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error("🚨 Fatal bootstrap error:", err);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,22 +1,10 @@
|
|||||||
"use client";
|
import { MDXRemote } from "next-mdx-remote/rsc";
|
||||||
|
|
||||||
import { useMDXComponent } from "next-contentlayer2/hooks";
|
|
||||||
import { mdxComponents } from "../content-engine/registry";
|
import { mdxComponents } from "../content-engine/registry";
|
||||||
|
|
||||||
interface MDXContentProps {
|
interface MDXContentProps {
|
||||||
code: string;
|
code: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MDXContent({ code }: MDXContentProps) {
|
export function MDXContent({ code }: MDXContentProps) {
|
||||||
// FIX: Contentlayer/MDX often appends hoisted functions *after* the `return MDXContent` statement,
|
return <MDXRemote source={code} components={mdxComponents} />;
|
||||||
// which causes Firefox to vomit hundreds of "unreachable code after return statement" warnings.
|
|
||||||
// We rewrite the generated IIFE string to move the return to the very end.
|
|
||||||
let patchedCode = code;
|
|
||||||
if (patchedCode.includes("return function MDXContent(")) {
|
|
||||||
patchedCode = patchedCode.replace("return function MDXContent(", "const MDXContent = function MDXContent(");
|
|
||||||
patchedCode += "\nreturn MDXContent;";
|
|
||||||
}
|
|
||||||
|
|
||||||
const Component = useMDXComponent(patchedCode);
|
|
||||||
return <Component components={mdxComponents} />;
|
|
||||||
}
|
}
|
||||||
|
|||||||
210
apps/web/src/components/blog/BlogClient.tsx
Normal file
210
apps/web/src/components/blog/BlogClient.tsx
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { MediumCard } from "../MediumCard";
|
||||||
|
import { BlogCommandBar } from "./BlogCommandBar";
|
||||||
|
import { Reveal } from "../Reveal";
|
||||||
|
import { Section } from "../Section";
|
||||||
|
import { AbstractCircuit, GradientMesh } from "../Effects";
|
||||||
|
import { useAnalytics } from "../analytics/useAnalytics";
|
||||||
|
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
|
||||||
|
interface PostType {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
date: string;
|
||||||
|
tags: string[];
|
||||||
|
slug: string;
|
||||||
|
thumbnail: string;
|
||||||
|
body: { code: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BlogClient({ allPosts }: { allPosts: PostType[] }) {
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [activeTags, setActiveTags] = useState<string[]>([]);
|
||||||
|
const { trackEvent } = useAnalytics();
|
||||||
|
|
||||||
|
const [filteredPosts, setFilteredPosts] = useState(allPosts);
|
||||||
|
|
||||||
|
// Memoize allTags
|
||||||
|
const allTags = React.useMemo(
|
||||||
|
() => Array.from(new Set(allPosts.flatMap((post) => post.tags || []))),
|
||||||
|
[allPosts],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [visibleCount, setVisibleCount] = useState(8);
|
||||||
|
|
||||||
|
const handleTagToggle = (tag: string) => {
|
||||||
|
setActiveTags((prev) =>
|
||||||
|
prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag],
|
||||||
|
);
|
||||||
|
setVisibleCount(8); // Reset pagination on filter change
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const query = searchQuery.toLowerCase().trim();
|
||||||
|
|
||||||
|
let filtered = allPosts;
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
filtered = filtered.filter((post) => {
|
||||||
|
const title = post.title.toLowerCase();
|
||||||
|
const description = post.description.toLowerCase();
|
||||||
|
const pTagString = (post.tags || []).join(" ").toLowerCase();
|
||||||
|
return (
|
||||||
|
title.includes(query) ||
|
||||||
|
description.includes(query) ||
|
||||||
|
pTagString.includes(query)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeTags.length > 0) {
|
||||||
|
filtered = filtered.filter((post) =>
|
||||||
|
post.tags?.some((tag) => activeTags.includes(tag)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFilteredPosts(filtered);
|
||||||
|
}, [searchQuery, activeTags, allPosts]);
|
||||||
|
|
||||||
|
const loadMore = () => {
|
||||||
|
setVisibleCount((prev) => prev + 6);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasMore = visibleCount < filteredPosts.length;
|
||||||
|
const postsToShow = filteredPosts.slice(0, visibleCount);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col bg-slate-50/30 overflow-hidden relative min-h-screen">
|
||||||
|
<AbstractCircuit />
|
||||||
|
|
||||||
|
{/* Header Section */}
|
||||||
|
<header className="relative pt-32 pb-8 md:pt-44 md:pb-12 z-20 overflow-hidden">
|
||||||
|
<GradientMesh
|
||||||
|
variant="metallic"
|
||||||
|
className="opacity-20 absolute inset-0 -z-10"
|
||||||
|
/>
|
||||||
|
<div className="narrow-container">
|
||||||
|
<div className="space-y-4 text-center">
|
||||||
|
<Reveal direction="down" delay={0.1}>
|
||||||
|
<div className="flex items-center justify-center gap-4 text-[10px] font-bold uppercase tracking-[0.3em] text-slate-400">
|
||||||
|
<div className="w-6 h-px bg-slate-200" />
|
||||||
|
<span>Knowledge Base</span>
|
||||||
|
<div className="w-12 h-px bg-slate-100" />
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
<Reveal delay={0.2}>
|
||||||
|
<div className="flex flex-col items-center gap-6">
|
||||||
|
<h1 className="text-5xl md:text-7xl font-bold text-slate-900 tracking-tighter leading-none">
|
||||||
|
Alle Artikel<span className="text-slate-300">.</span>
|
||||||
|
</h1>
|
||||||
|
<p className="font-serif italic text-slate-400 text-sm md:text-xl max-w-sm">
|
||||||
|
Gedanken über Engineering, Design und die Architektur der
|
||||||
|
Zukunft.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Sticky Filter Bar */}
|
||||||
|
<div className="sticky top-0 z-40 bg-slate-50/80 backdrop-blur-xl border-y border-slate-200/50 py-4 shadow-sm transition-all duration-300">
|
||||||
|
<div className="narrow-container">
|
||||||
|
<Reveal width="100%">
|
||||||
|
<BlogCommandBar
|
||||||
|
searchQuery={searchQuery}
|
||||||
|
onSearchChange={setSearchQuery}
|
||||||
|
tags={allTags}
|
||||||
|
activeTags={activeTags}
|
||||||
|
onTagToggle={handleTagToggle}
|
||||||
|
/>
|
||||||
|
</Reveal>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Section
|
||||||
|
className="pb-32 pt-12"
|
||||||
|
containerVariant="narrow"
|
||||||
|
variant="white"
|
||||||
|
>
|
||||||
|
<div className="space-y-12 relative z-10 p-4 md:p-0">
|
||||||
|
{/* Posts List (Vertical & Minimal) */}
|
||||||
|
<div id="posts-container" className="space-y-12">
|
||||||
|
<AnimatePresence mode="popLayout" initial={false}>
|
||||||
|
{postsToShow.length === 0 ? (
|
||||||
|
<motion.div
|
||||||
|
key="no-results"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.95 }}
|
||||||
|
className="py-24 text-center border border-dashed border-slate-200 rounded-3xl bg-white/50"
|
||||||
|
>
|
||||||
|
<p className="text-slate-400 font-mono text-sm uppercase tracking-widest">
|
||||||
|
Keine Beiträge gefunden.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setSearchQuery("");
|
||||||
|
setActiveTags([]);
|
||||||
|
}}
|
||||||
|
className="mt-4 text-xs font-bold text-slate-900 underline underline-offset-4 hover:text-slate-600"
|
||||||
|
>
|
||||||
|
Filter zurücksetzen
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<motion.div
|
||||||
|
key="post-grid"
|
||||||
|
layout
|
||||||
|
className="grid grid-cols-1 gap-6 w-full"
|
||||||
|
>
|
||||||
|
<AnimatePresence mode="popLayout">
|
||||||
|
{postsToShow.map((post, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={post.slug}
|
||||||
|
layout
|
||||||
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.9 }}
|
||||||
|
transition={{
|
||||||
|
duration: 0.4,
|
||||||
|
delay: i < 8 ? i * 0.05 : 0,
|
||||||
|
ease: [0.16, 1, 0.3, 1],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MediumCard post={post as any} />
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
|
{hasMore && (
|
||||||
|
<div className="flex justify-center pt-8">
|
||||||
|
<Reveal delay={0.1} width="fit-content">
|
||||||
|
<button
|
||||||
|
onClick={loadMore}
|
||||||
|
className="group relative px-8 py-4 bg-white border border-slate-200 text-slate-600 rounded-full overflow-hidden transition-all hover:border-slate-400 hover:text-slate-900 hover:shadow-lg"
|
||||||
|
>
|
||||||
|
<span className="relative z-10 font-mono text-xs uppercase tracking-widest flex items-center gap-3">
|
||||||
|
Mehr laden
|
||||||
|
<div className="w-1 h-1 bg-slate-300 rounded-full group-hover:bg-slate-900 transition-colors" />
|
||||||
|
<div className="w-1 h-1 bg-slate-300 rounded-full group-hover:bg-slate-900 transition-colors delay-75" />
|
||||||
|
<div className="w-1 h-1 bg-slate-300 rounded-full group-hover:bg-slate-900 transition-colors delay-150" />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</Reveal>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
30
apps/web/src/lib/posts.ts
Normal file
30
apps/web/src/lib/posts.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { getPayload } from "payload";
|
||||||
|
import configPromise from "@payload-config";
|
||||||
|
|
||||||
|
export async function getAllPosts() {
|
||||||
|
if (!process.env.DATABASE_URI && !process.env.POSTGRES_URI) {
|
||||||
|
console.warn(
|
||||||
|
"⚠️ Bypassing Payload fetch during Next.js build: DATABASE_URI is missing.",
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = await getPayload({ config: configPromise });
|
||||||
|
const { docs } = await payload.find({
|
||||||
|
collection: "posts",
|
||||||
|
limit: 1000,
|
||||||
|
sort: "-date",
|
||||||
|
});
|
||||||
|
|
||||||
|
return docs.map((doc) => ({
|
||||||
|
title: doc.title as string,
|
||||||
|
description: doc.description as string,
|
||||||
|
date: doc.date as string,
|
||||||
|
tags: (doc.tags || []).map((t) =>
|
||||||
|
typeof t === "object" && t !== null ? t.tag : t,
|
||||||
|
) as string[],
|
||||||
|
slug: doc.slug as string,
|
||||||
|
thumbnail: doc.thumbnail as string,
|
||||||
|
body: { code: doc.content as string },
|
||||||
|
}));
|
||||||
|
}
|
||||||
47
apps/web/src/payload/collections/Media.ts
Normal file
47
apps/web/src/payload/collections/Media.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import type { CollectionConfig } from "payload";
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
const filename = fileURLToPath(import.meta.url);
|
||||||
|
const dirname = path.dirname(filename);
|
||||||
|
|
||||||
|
export const Media: CollectionConfig = {
|
||||||
|
slug: "media",
|
||||||
|
admin: {
|
||||||
|
useAsTitle: "alt",
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
read: () => true, // Publicly readable
|
||||||
|
},
|
||||||
|
upload: {
|
||||||
|
staticDir: path.resolve(dirname, "../../../../public/media"),
|
||||||
|
adminThumbnail: "thumbnail",
|
||||||
|
imageSizes: [
|
||||||
|
{
|
||||||
|
name: "thumbnail",
|
||||||
|
width: 400,
|
||||||
|
height: 300,
|
||||||
|
position: "centre",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "card",
|
||||||
|
width: 768,
|
||||||
|
height: 1024,
|
||||||
|
position: "centre",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tablet",
|
||||||
|
width: 1024,
|
||||||
|
height: undefined,
|
||||||
|
position: "centre",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "alt",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
74
apps/web/src/payload/collections/Posts.ts
Normal file
74
apps/web/src/payload/collections/Posts.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import type { CollectionConfig } from "payload";
|
||||||
|
|
||||||
|
export const Posts: CollectionConfig = {
|
||||||
|
slug: "posts",
|
||||||
|
admin: {
|
||||||
|
useAsTitle: "title",
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
read: () => true, // Publicly readable API
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
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: "thumbnail",
|
||||||
|
type: "text", // Keeping as text for now to match current MDX strings like "/blog/green-it.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "content",
|
||||||
|
type: "code",
|
||||||
|
admin: {
|
||||||
|
language: "markdown",
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
13
apps/web/src/payload/collections/Users.ts
Normal file
13
apps/web/src/payload/collections/Users.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import type { CollectionConfig } from "payload";
|
||||||
|
|
||||||
|
export const Users: CollectionConfig = {
|
||||||
|
slug: "users",
|
||||||
|
admin: {
|
||||||
|
useAsTitle: "email",
|
||||||
|
},
|
||||||
|
auth: true,
|
||||||
|
fields: [
|
||||||
|
// Email added by default due to auth
|
||||||
|
// Add more fields as needed
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -69,29 +69,23 @@ export function getImgproxyUrl(
|
|||||||
"http://directus:8055",
|
"http://directus:8055",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const { width = 0, height = 0, enlarge = false, extension = "" } = options;
|
||||||
width = 0,
|
|
||||||
height = 0,
|
|
||||||
resizing_type = "fit",
|
|
||||||
gravity = "sm", // Default to smart gravity
|
|
||||||
enlarge = false,
|
|
||||||
extension = "",
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
// Processing options
|
let quality = 80;
|
||||||
// Format: /rs:<type>:<width>:<height>:<enlarge>/g:<gravity>
|
if (extension) quality = 90;
|
||||||
const processingOptions = [
|
|
||||||
`rs:${resizing_type}:${width}:${height}:${enlarge ? 1 : 0}`,
|
|
||||||
`g:${gravity}`,
|
|
||||||
].join("/");
|
|
||||||
|
|
||||||
// Using /unsafe/ for now as we don't handle signatures yet
|
// Re-map imgproxy URL to our new parameter structure
|
||||||
// Format: <base_url>/unsafe/<options>/<base64_url>
|
// e.g. /process?url=...&w=...&h=...&q=...&format=...
|
||||||
const suffix = extension ? `@${extension}` : "";
|
const queryParams = new URLSearchParams({
|
||||||
const encodedSrc = encodeBase64(absoluteSrc + suffix);
|
url: absoluteSrc,
|
||||||
|
});
|
||||||
|
|
||||||
return `${baseUrl}/unsafe/${processingOptions}/${encodedSrc}`;
|
if (width > 0) queryParams.set("w", width.toString());
|
||||||
|
if (height > 0) queryParams.set("h", height.toString());
|
||||||
|
if (extension) queryParams.set("format", extension.replace(".", ""));
|
||||||
|
if (quality) queryParams.set("q", quality.toString());
|
||||||
|
|
||||||
|
return `${baseUrl}/process?${queryParams.toString()}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,8 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": ["./*"],
|
||||||
"./*"
|
"@payload-config": ["./payload.config.ts"]
|
||||||
],
|
|
||||||
"contentlayer/generated": [
|
|
||||||
"./.contentlayer/generated"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
@@ -18,7 +14,5 @@
|
|||||||
".next/types/**/*.ts",
|
".next/types/**/*.ts",
|
||||||
".contentlayer/generated"
|
".contentlayer/generated"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": ["node_modules"]
|
||||||
"node_modules"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
@@ -34,35 +34,7 @@ services:
|
|||||||
- "caddy=http://gatekeeper.${TRAEFIK_HOST:-mintel.localhost}"
|
- "caddy=http://gatekeeper.${TRAEFIK_HOST:-mintel.localhost}"
|
||||||
- "caddy.reverse_proxy={{upstreams 3000}}"
|
- "caddy.reverse_proxy={{upstreams 3000}}"
|
||||||
|
|
||||||
directus:
|
postgres-db:
|
||||||
image: registry.infra.mintel.me/mintel/directus:latest
|
|
||||||
restart: always
|
|
||||||
networks:
|
|
||||||
- default
|
|
||||||
- infra
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
|
||||||
KEY: ${DIRECTUS_KEY}
|
|
||||||
SECRET: ${DIRECTUS_SECRET}
|
|
||||||
DB_CLIENT: "pg"
|
|
||||||
DB_HOST: "directus-db"
|
|
||||||
DB_PORT: "5432"
|
|
||||||
DB_DATABASE: ${DIRECTUS_DB_NAME:-directus}
|
|
||||||
DB_USER: ${DIRECTUS_DB_USER:-directus}
|
|
||||||
DB_PASSWORD: ${DIRECTUS_DB_PASSWORD:-directus}
|
|
||||||
PUBLIC_URL: ${DIRECTUS_URL:-https://cms.mintel.me}
|
|
||||||
volumes:
|
|
||||||
- ./directus/uploads:/directus/uploads
|
|
||||||
- ./directus/extensions:/directus/extensions
|
|
||||||
- ./directus/migrations:/directus/migrations
|
|
||||||
labels:
|
|
||||||
- "traefik.http.services.${PROJECT_NAME:-mintel-me}-directus.loadbalancer.server.port=8055"
|
|
||||||
- "traefik.docker.network=infra"
|
|
||||||
- "caddy=http://${DIRECTUS_HOST:-cms.mintel.localhost}"
|
|
||||||
- "caddy.reverse_proxy={{upstreams 8055}}"
|
|
||||||
|
|
||||||
directus-db:
|
|
||||||
image: postgres:15-alpine
|
image: postgres:15-alpine
|
||||||
restart: always
|
restart: always
|
||||||
networks:
|
networks:
|
||||||
@@ -70,9 +42,11 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: ${DIRECTUS_DB_NAME:-directus}
|
POSTGRES_DB: ${postgres_DB_NAME:-directus}
|
||||||
POSTGRES_USER: ${DIRECTUS_DB_USER:-directus}
|
POSTGRES_USER: ${postgres_DB_USER:-directus}
|
||||||
POSTGRES_PASSWORD: ${DIRECTUS_DB_PASSWORD:-directus}
|
POSTGRES_PASSWORD: ${postgres_DB_PASSWORD:-directus}
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- directus-db-data:/var/lib/postgresql/data
|
- directus-db-data:/var/lib/postgresql/data
|
||||||
|
|
||||||
@@ -84,10 +58,9 @@ services:
|
|||||||
- infra
|
- infra
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- "mintel.localhost:host-gateway"
|
- "mintel.localhost:host-gateway"
|
||||||
- "cms.mintel.localhost:host-gateway"
|
|
||||||
- "host.docker.internal:host-gateway"
|
- "host.docker.internal:host-gateway"
|
||||||
environment:
|
environment:
|
||||||
IMGPROXY_URL_MAPPING: "http://mintel.localhost/:http://app:3000/,http://cms.mintel.localhost/:http://directus:8055/"
|
IMGPROXY_URL_MAPPING: "http://mintel.localhost/:http://app:3000/"
|
||||||
IMGPROXY_USE_ETAG: "true"
|
IMGPROXY_USE_ETAG: "true"
|
||||||
IMGPROXY_MAX_SRC_RESOLUTION: 20
|
IMGPROXY_MAX_SRC_RESOLUTION: 20
|
||||||
IMGPROXY_ALLOWED_NETWORKS: "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
|
IMGPROXY_ALLOWED_NETWORKS: "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ services:
|
|||||||
- "traefik.http.middlewares.mintel-me-forward.headers.customrequestheaders.X-Forwarded-Ssl=on"
|
- "traefik.http.middlewares.mintel-me-forward.headers.customrequestheaders.X-Forwarded-Ssl=on"
|
||||||
|
|
||||||
mintel-me-gatekeeper:
|
mintel-me-gatekeeper:
|
||||||
profiles: [ "gatekeeper" ]
|
profiles: ["gatekeeper"]
|
||||||
image: registry.infra.mintel.me/mintel/gatekeeper:v1.7.12
|
image: registry.infra.mintel.me/mintel/gatekeeper:v1.7.12
|
||||||
container_name: ${PROJECT_NAME:-mintel-me}-gatekeeper
|
container_name: ${PROJECT_NAME:-mintel-me}-gatekeeper
|
||||||
restart: always
|
restart: always
|
||||||
@@ -79,52 +79,7 @@ services:
|
|||||||
- "caddy=gatekeeper.${TRAEFIK_HOST:-mintel.localhost}"
|
- "caddy=gatekeeper.${TRAEFIK_HOST:-mintel.localhost}"
|
||||||
- "caddy.reverse_proxy={{upstreams 3000}}"
|
- "caddy.reverse_proxy={{upstreams 3000}}"
|
||||||
|
|
||||||
mintel-me-cms:
|
postgres-db:
|
||||||
image: registry.infra.mintel.me/mintel/directus:latest
|
|
||||||
restart: always
|
|
||||||
networks:
|
|
||||||
- default
|
|
||||||
- infra
|
|
||||||
env_file:
|
|
||||||
- ${ENV_FILE:-.env}
|
|
||||||
environment:
|
|
||||||
KEY: ${DIRECTUS_KEY}
|
|
||||||
SECRET: ${DIRECTUS_SECRET}
|
|
||||||
DB_CLIENT: "pg"
|
|
||||||
DB_HOST: "mintel-me-db"
|
|
||||||
DB_PORT: "5432"
|
|
||||||
DB_DATABASE: ${DIRECTUS_DB_NAME:-directus}
|
|
||||||
DB_USER: ${DIRECTUS_DB_USER:-directus}
|
|
||||||
DB_PASSWORD: ${DIRECTUS_DB_PASSWORD:-directus}
|
|
||||||
PUBLIC_URL: ${DIRECTUS_URL:-https://cms.mintel.me}
|
|
||||||
SENTRY_DSN: ${SENTRY_DSN}
|
|
||||||
SENTRY_ENVIRONMENT: ${TARGET:-development}
|
|
||||||
LOGGER_LEVEL: ${LOG_LEVEL:-info}
|
|
||||||
volumes:
|
|
||||||
- ./directus/uploads:/directus/uploads
|
|
||||||
- ./directus/extensions:/directus/extensions
|
|
||||||
- ./directus/schema:/directus/schema
|
|
||||||
- ./directus/migrations:/directus/migrations
|
|
||||||
healthcheck:
|
|
||||||
test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:8055/server/ping" ]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 3
|
|
||||||
start_period: 30s
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
- 'traefik.http.routers.mintel-me-cms.rule=${TRAEFIK_DIRECTUS_RULE:-Host("${DIRECTUS_HOST:-cms.mintel.localhost}")}'
|
|
||||||
- "traefik.http.routers.mintel-me-cms.entrypoints=${TRAEFIK_ENTRYPOINT:-web}"
|
|
||||||
- "traefik.http.routers.mintel-me-cms.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-}"
|
|
||||||
- "traefik.http.routers.mintel-me-cms.tls=${TRAEFIK_TLS:-false}"
|
|
||||||
- "traefik.http.routers.mintel-me-cms.service=mintel-me-cms-svc"
|
|
||||||
- "traefik.http.routers.mintel-me-cms.middlewares=mintel-me-forward"
|
|
||||||
- "traefik.http.services.mintel-me-cms-svc.loadbalancer.server.port=8055"
|
|
||||||
- "traefik.docker.network=infra"
|
|
||||||
- "caddy=${DIRECTUS_HOST:-cms.mintel.localhost}"
|
|
||||||
- "caddy.reverse_proxy={{upstreams 8055}}"
|
|
||||||
|
|
||||||
mintel-me-db:
|
|
||||||
image: postgres:15-alpine
|
image: postgres:15-alpine
|
||||||
restart: always
|
restart: always
|
||||||
networks:
|
networks:
|
||||||
@@ -132,9 +87,9 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- ${ENV_FILE:-.env}
|
- ${ENV_FILE:-.env}
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: ${DIRECTUS_DB_NAME:-directus}
|
POSTGRES_DB: ${postgres_DB_NAME:-directus}
|
||||||
POSTGRES_USER: ${DIRECTUS_DB_USER:-directus}
|
POSTGRES_USER: ${postgres_DB_USER:-directus}
|
||||||
POSTGRES_PASSWORD: ${DIRECTUS_DB_PASSWORD:-directus}
|
POSTGRES_PASSWORD: ${postgres_DB_PASSWORD:-directus}
|
||||||
volumes:
|
volumes:
|
||||||
- directus-db-data:/var/lib/postgresql/data
|
- directus-db-data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "pnpm@10.18.3",
|
"packageManager": "pnpm@10.18.3",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "docker network create infra 2>/dev/null || true && echo \"\n🚀 Development Environment Starting...\n\n📱 App: http://mintel.localhost\n🗄️ CMS: http://cms.mintel.localhost/admin\n🖼️ Imgproxy: http://img.mintel.localhost\n🚦 Caddy Proxy: http://localhost:80\n\" && lsof -ti:3000 | xargs kill -9 2>/dev/null || true && rm -rf apps/web/.next apps/web/.contentlayer/.cache 2>/dev/null || true && docker rm -f mintel-me-gatekeeper 2>/dev/null || true && COMPOSE_PROJECT_NAME=mintel-me docker-compose -f docker-compose.dev.yml down --remove-orphans && COMPOSE_PROJECT_NAME=mintel-me docker-compose -f docker-compose.dev.yml up -d app directus directus-db gatekeeper imgproxy && pnpm -r dev",
|
"dev": "docker network create infra 2>/dev/null || true && echo \"\\n🚀 Development Environment Starting...\\n\\n📱 App: http://mintel.localhost\\n🖼️ Imgproxy: http://img.mintel.localhost\\n🚦 Caddy Proxy: http://localhost:80\\n\" && lsof -ti:3000 | xargs kill -9 2>/dev/null || true && rm -rf apps/web/.next apps/web/.contentlayer/.cache 2>/dev/null || true && docker rm -f mintel-me-gatekeeper 2>/dev/null || true && COMPOSE_PROJECT_NAME=mintel-me docker-compose -f docker-compose.dev.yml down --remove-orphans && COMPOSE_PROJECT_NAME=mintel-me docker-compose -f docker-compose.dev.yml up -d app postgres-db gatekeeper imgproxy && DATABASE_URI=\"postgres://directus:directus@localhost:5432/directus\" PAYLOAD_SECRET=\"dev-secret\" pnpm -r dev",
|
||||||
"dev:clean": "pnpm dev:stop && rm -rf apps/web/.next apps/web/.contentlayer apps/web/node_modules && pnpm install && pnpm dev",
|
"dev:clean": "pnpm dev:stop && rm -rf apps/web/.next apps/web/.contentlayer apps/web/node_modules && pnpm install && pnpm dev",
|
||||||
"dev:stop": "lsof -ti:3000,3001,3002 | xargs kill -9 2>/dev/null || true && COMPOSE_PROJECT_NAME=mintel-me docker-compose -f docker-compose.dev.yml down",
|
"dev:stop": "lsof -ti:3000,3001,3002 | xargs kill -9 2>/dev/null || true && COMPOSE_PROJECT_NAME=mintel-me docker-compose -f docker-compose.dev.yml down",
|
||||||
"dev:local": "pnpm -r dev",
|
"dev:local": "pnpm -r dev",
|
||||||
@@ -48,7 +48,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@directus/sdk": "21.0.0",
|
|
||||||
"@eslint/compat": "^2.0.2",
|
"@eslint/compat": "^2.0.2",
|
||||||
"@mintel/acquisition": "link:../at-mintel/packages/acquisition-library",
|
"@mintel/acquisition": "link:../at-mintel/packages/acquisition-library",
|
||||||
"tsx": "^4.21.0"
|
"tsx": "^4.21.0"
|
||||||
|
|||||||
4536
pnpm-lock.yaml
generated
4536
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,116 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
REMOTE_HOST="${SSH_HOST:-root@alpha.mintel.me}"
|
|
||||||
ACTION=$1
|
|
||||||
ENV=$2
|
|
||||||
|
|
||||||
# Help
|
|
||||||
if [ -z "$ACTION" ] || [ -z "$ENV" ]; then
|
|
||||||
echo "Usage: ./scripts/sync-directus.sh [push|pull] [testing|staging|production]"
|
|
||||||
echo ""
|
|
||||||
echo "Commands:"
|
|
||||||
echo " push Sync LOCAL data -> REMOTE"
|
|
||||||
echo " pull Sync REMOTE data -> LOCAL"
|
|
||||||
echo ""
|
|
||||||
echo "Environments:"
|
|
||||||
echo " testing, staging, production"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Project Configuration
|
|
||||||
PRJ_ID="mintel-me"
|
|
||||||
REMOTE_DIR="/home/deploy/sites/mintel.me"
|
|
||||||
|
|
||||||
case $ENV in
|
|
||||||
testing) PROJECT_NAME="${PRJ_ID}-testing"; ENV_FILE=".env.testing" ;;
|
|
||||||
staging) PROJECT_NAME="${PRJ_ID}-staging"; ENV_FILE=".env.staging" ;;
|
|
||||||
production) PROJECT_NAME="${PRJ_ID}-prod"; ENV_FILE=".env.prod" ;;
|
|
||||||
*) echo "❌ Invalid environment: $ENV"; exit 1 ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# DB Details
|
|
||||||
DB_USER="directus"
|
|
||||||
DB_NAME="directus"
|
|
||||||
|
|
||||||
echo "🔍 Detecting local database..."
|
|
||||||
# Check root or apps/web for docker-compose context
|
|
||||||
if [ -f "docker-compose.yml" ]; then
|
|
||||||
COMPOSE_CMD="docker compose"
|
|
||||||
elif [ -f "../../docker-compose.yml" ]; then
|
|
||||||
COMPOSE_CMD="docker compose -f ../../docker-compose.yml"
|
|
||||||
else
|
|
||||||
echo "❌ docker-compose.yml not found."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
LOCAL_DB_CONTAINER=$($COMPOSE_CMD ps -q directus-db)
|
|
||||||
if [ -z "$LOCAL_DB_CONTAINER" ]; then
|
|
||||||
echo "❌ Local directus-db container not found. Is it running?"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$ACTION" == "push" ]; then
|
|
||||||
echo "🚀 Pushing LOCAL -> $ENV ($PROJECT_NAME)..."
|
|
||||||
|
|
||||||
echo "📦 Dumping local database..."
|
|
||||||
docker exec "$LOCAL_DB_CONTAINER" pg_dump -U "$DB_USER" --clean --if-exists --no-owner --no-privileges "$DB_NAME" > dump.sql
|
|
||||||
|
|
||||||
echo "📤 Uploading dump to remote server..."
|
|
||||||
scp dump.sql "$REMOTE_HOST:$REMOTE_DIR/dump.sql"
|
|
||||||
|
|
||||||
echo "🔄 Restoring dump on $ENV..."
|
|
||||||
REMOTE_DB_CONTAINER=$(ssh "$REMOTE_HOST" "cd $REMOTE_DIR && docker compose -p $PROJECT_NAME ps -q directus-db")
|
|
||||||
|
|
||||||
if [ -z "$REMOTE_DB_CONTAINER" ]; then
|
|
||||||
echo "❌ Remote $ENV-db container not found!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
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;'"
|
|
||||||
|
|
||||||
echo "⚡ Restoring database..."
|
|
||||||
ssh "$REMOTE_HOST" "docker exec -i $REMOTE_DB_CONTAINER psql -U $DB_USER $DB_NAME < $REMOTE_DIR/dump.sql"
|
|
||||||
|
|
||||||
echo "📁 Syncing uploads (Local -> $ENV)..."
|
|
||||||
rsync -avz --progress ./directus/uploads/ "$REMOTE_HOST:$REMOTE_DIR/directus/uploads/"
|
|
||||||
|
|
||||||
rm dump.sql
|
|
||||||
ssh "$REMOTE_HOST" "rm $REMOTE_DIR/dump.sql"
|
|
||||||
|
|
||||||
echo "🔄 Restarting remote Directus..."
|
|
||||||
ssh "$REMOTE_HOST" "cd $REMOTE_DIR && docker compose -p $PROJECT_NAME restart directus"
|
|
||||||
|
|
||||||
echo "✨ Push to $ENV complete!"
|
|
||||||
|
|
||||||
elif [ "$ACTION" == "pull" ]; then
|
|
||||||
echo "📥 Pulling $ENV Data -> LOCAL..."
|
|
||||||
|
|
||||||
echo "📦 Dumping remote database ($ENV)..."
|
|
||||||
REMOTE_DB_CONTAINER=$(ssh "$REMOTE_HOST" "cd $REMOTE_DIR && docker compose -p $PROJECT_NAME ps -q directus-db")
|
|
||||||
|
|
||||||
if [ -z "$REMOTE_DB_CONTAINER" ]; then
|
|
||||||
echo "❌ Remote $ENV-db container not found!"
|
|
||||||
exit 1
|
|
||||||
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"
|
|
||||||
|
|
||||||
echo "📥 Downloading dump..."
|
|
||||||
scp "$REMOTE_HOST:$REMOTE_DIR/dump.sql" dump.sql
|
|
||||||
|
|
||||||
echo "🧹 Wiping local database schema..."
|
|
||||||
docker exec "$LOCAL_DB_CONTAINER" psql -U "$DB_USER" "$DB_NAME" -c 'DROP SCHEMA public CASCADE; CREATE SCHEMA public;'
|
|
||||||
|
|
||||||
echo "⚡ Restoring database locally..."
|
|
||||||
docker exec -i "$LOCAL_DB_CONTAINER" psql -U "$DB_USER" "$DB_NAME" < dump.sql
|
|
||||||
|
|
||||||
echo "📁 Syncing uploads ($ENV -> Local)..."
|
|
||||||
rsync -avz --progress "$REMOTE_HOST:$REMOTE_DIR/directus/uploads/" ./directus/uploads/
|
|
||||||
|
|
||||||
rm dump.sql
|
|
||||||
ssh "$REMOTE_HOST" "rm $REMOTE_DIR/dump.sql"
|
|
||||||
|
|
||||||
echo "✨ Pull to Local complete!"
|
|
||||||
fi
|
|
||||||
Reference in New Issue
Block a user