diff --git a/.env.example b/.env.example index 0c56f2de..21e8a7e4 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,6 @@ -ETERNALCODE_STRAPI_URL=https://localhost:1337 # Replace with your Strapi URL -ETERNALCODE_STRAPI_KEY=YOUR_STRAPI_KEY # Replace with your Strapi key \ No newline at end of file +# Server URL (required for PayloadCMS to generate correct media URLs) +NEXT_PUBLIC_SERVER_URL=http://localhost:3000 + +# PayloadCMS Configuration +PAYLOAD_SECRET=your-secret-key-that-is-very-long-and-secure +DATABASE_URI=file:./payload.db \ No newline at end of file diff --git a/.gitignore b/.gitignore index b88d084b..a9d927e1 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,8 @@ next-env.d.ts # ignore idea files .idea + +# Shannon +.shannon-tool +shannon_repos +shannon_results diff --git a/app/(payload)/admin/[[...segments]]/page.tsx b/app/(payload)/admin/[[...segments]]/page.tsx new file mode 100644 index 00000000..09d2719a --- /dev/null +++ b/app/(payload)/admin/[[...segments]]/page.tsx @@ -0,0 +1,24 @@ +/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ +/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ + +import { generatePageMetadata, RootPage } from "@payloadcms/next/views"; +import type { Metadata } from "next"; +import config from "@/payload.config"; +import { importMap } from "../importMap"; + +type Args = { + params: Promise<{ + segments: string[]; + }>; + searchParams: Promise<{ + [key: string]: string | string[]; + }>; +}; + +export const generateMetadata = ({ params, searchParams }: Args): Promise => + generatePageMetadata({ config, params, searchParams }); + +const Page = ({ params, searchParams }: Args) => + RootPage({ config, params, searchParams, importMap }); + +export default Page; diff --git a/app/(payload)/admin/importMap.js b/app/(payload)/admin/importMap.js new file mode 100644 index 00000000..31a4ea72 --- /dev/null +++ b/app/(payload)/admin/importMap.js @@ -0,0 +1,55 @@ +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 { IconPickerField as IconPickerField_0e51d24112b0c7a9183e4beed306e02c } from '../../../components/payload/icon-picker-field' +import { ColorPickerField as ColorPickerField_13aa4039160d1009a0c4ef1c0c20fcfb } from '../../../components/payload/color-picker-field' +import { VercelBlobClientUploadHandler as VercelBlobClientUploadHandler_16c82c5e25f430251a3e3ba57219ff4e } from '@payloadcms/storage-vercel-blob/client' + +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, + "/components/payload/icon-picker-field#IconPickerField": IconPickerField_0e51d24112b0c7a9183e4beed306e02c, + "/components/payload/color-picker-field#ColorPickerField": ColorPickerField_13aa4039160d1009a0c4ef1c0c20fcfb, + "@payloadcms/storage-vercel-blob/client#VercelBlobClientUploadHandler": VercelBlobClientUploadHandler_16c82c5e25f430251a3e3ba57219ff4e +} diff --git a/app/(payload)/api/[...slug]/route.ts b/app/(payload)/api/[...slug]/route.ts new file mode 100644 index 00000000..b93dc813 --- /dev/null +++ b/app/(payload)/api/[...slug]/route.ts @@ -0,0 +1,19 @@ +/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ +/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ +import config from "@/payload.config"; +import "@payloadcms/next/css"; +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 PATCH = REST_PATCH(config); +export const PUT = REST_PUT(config); +export const OPTIONS = REST_OPTIONS(config); diff --git a/app/(payload)/api/graphql/route.ts b/app/(payload)/api/graphql/route.ts new file mode 100644 index 00000000..05323fab --- /dev/null +++ b/app/(payload)/api/graphql/route.ts @@ -0,0 +1,5 @@ +import config from "@payload-config"; +import { GRAPHQL_PLAYGROUND_GET, GRAPHQL_POST } from "@payloadcms/next/routes"; + +export const GET = GRAPHQL_PLAYGROUND_GET(config); +export const POST = GRAPHQL_POST(config); diff --git a/app/(payload)/layout.tsx b/app/(payload)/layout.tsx new file mode 100644 index 00000000..c473ee49 --- /dev/null +++ b/app/(payload)/layout.tsx @@ -0,0 +1,32 @@ +import type { ServerRuntime } from "next"; +import type { ServerFunctionClient } from "payload"; + +import config from "@/payload.config"; +import "@payloadcms/next/css"; +import { handleServerFunctions, RootLayout } from "@payloadcms/next/layouts"; +import type React from "react"; + +import { importMap } from "./admin/importMap"; + +export const runtime: ServerRuntime = "nodejs"; + +type Args = { + children: React.ReactNode; +}; + +const serverFunction: ServerFunctionClient = async (args) => { + "use server"; + return handleServerFunctions({ + ...args, + config, + importMap, + }); +}; + +export default function Layout({ children }: Args) { + return ( + + {children} + + ); +} diff --git a/app/(website)/[...not-found]/page.tsx b/app/(website)/[...not-found]/page.tsx new file mode 100644 index 00000000..d330d70e --- /dev/null +++ b/app/(website)/[...not-found]/page.tsx @@ -0,0 +1,5 @@ +import { notFound } from "next/navigation"; + +export default function NotFoundCatchAll() { + notFound(); +} diff --git a/app/(website)/api/builds/builds.ts b/app/(website)/api/builds/builds.ts new file mode 100644 index 00000000..07f33c93 --- /dev/null +++ b/app/(website)/api/builds/builds.ts @@ -0,0 +1,160 @@ +import { z } from "zod"; + +export const BuildArtifactSchema = z.object({ + id: z.number(), + node_id: z.string(), + name: z.string(), + size_in_bytes: z.number(), + url: z.string(), + archive_download_url: z.string(), + expired: z.boolean(), + created_at: z.string(), + expires_at: z.string(), + updated_at: z.string(), +}); + +export type BuildArtifact = z.infer; + +export const BuildRunSchema = z.object({ + id: z.number(), + name: z.string().nullable(), + status: z.string(), + conclusion: z.string().nullable(), + head_branch: z.string(), + head_sha: z.string(), + created_at: z.string(), + html_url: z.string(), + artifacts_url: z.string(), + display_title: z.string().optional(), +}); + +export type BuildRun = z.infer & { + found_artifact?: BuildArtifact; +}; + +const GithubRunsResponseSchema = z.object({ + workflow_runs: z.array(BuildRunSchema), +}); + +const BuildArtifactsResponseSchema = z.object({ + total_count: z.number(), + artifacts: z.array(BuildArtifactSchema), +}); + +export const ModrinthFileSchema = z.object({ + url: z.string(), + filename: z.string(), + primary: z.boolean(), +}); + +export const ModrinthVersionSchema = z.object({ + id: z.string(), + name: z.string(), + version_number: z.string(), + date_published: z.string(), + files: z.array(ModrinthFileSchema), +}); + +export type ModrinthVersion = z.infer; + +export type Project = { + id: string; + name: string; + githubRepo: string; + modrinthId?: string; +}; + +export const PROJECTS: Project[] = [ + { + id: "eternalcore", + name: "EternalCore", + githubRepo: "EternalCodeTeam/EternalCore", + modrinthId: "eternalcore", + }, + { + id: "eternalcombat", + name: "EternalCombat", + githubRepo: "EternalCodeTeam/EternalCombat", + modrinthId: "eternalcombat", + }, +]; + +export async function fetchDevBuilds(project: Project): Promise { + try { + const res = await fetch( + `https://api.github.com/repos/${project.githubRepo}/actions/runs?per_page=20&status=success&branch=master` + ); + if (!res.ok) { + console.error(`Failed to fetch Github Actions for ${project.name}`, await res.text()); + return []; + } + + const json = await res.json(); + const parsed = GithubRunsResponseSchema.safeParse(json); + + if (!parsed.success) { + console.error(`Invalid Github Actions response for ${project.name}`, parsed.error); + return []; + } + + const runs = parsed.data.workflow_runs; + + // Fetch artifacts for each run to get the correct artifact name + return await Promise.all( + runs.map(async (run) => { + try { + const artRes = await fetch(run.artifacts_url); + if (!artRes.ok) { + return run; + } + + const artJson = await artRes.json(); + const artParsed = BuildArtifactsResponseSchema.safeParse(artJson); + + if (artParsed.success && artParsed.data.artifacts.length > 0) { + // We take the first artifact as the primary one + return { ...run, found_artifact: artParsed.data.artifacts[0] }; + } + return run; + } catch (e) { + console.error(`Error fetching artifacts for run ${run.id}`, e); + return run; + } + }) + ); + } catch (error) { + console.error(`Error fetching dev builds for ${project.name}`, error); + return []; + } +} + +export async function fetchStableBuilds(project: Project): Promise { + if (!project.modrinthId) { + return []; + } + + try { + const res = await fetch(`https://api.modrinth.com/v2/project/${project.modrinthId}/version`); + if (!res.ok) { + if (res.status === 404) { + return []; // Project might not exist yet + } + console.error(`Failed to fetch Modrinth versions for ${project.name}`, await res.text()); + return []; + } + + const json = await res.json(); + // Validate response is an array of ModrinthVersion + const parsed = z.array(ModrinthVersionSchema).safeParse(json); + + if (!parsed.success) { + console.error(`Invalid Modrinth versions response for ${project.name}`, parsed.error); + return []; + } + + return parsed.data; + } catch (error) { + console.error(`Error fetching stable builds for ${project.name}`, error); + return []; + } +} diff --git a/app/api/docs/search-index/route.ts b/app/(website)/api/docs/search-index/route.ts similarity index 84% rename from app/api/docs/search-index/route.ts rename to app/(website)/api/docs/search-index/route.ts index e82bbea2..6a80e132 100644 --- a/app/api/docs/search-index/route.ts +++ b/app/(website)/api/docs/search-index/route.ts @@ -4,6 +4,14 @@ import path from "node:path"; import matter from "gray-matter"; import { NextResponse } from "next/server"; +const MDX_EXTENSION_REGEX = /\.mdx$/; + +type SearchIndexItem = { + title: string; + path: string; + excerpt: string; +}; + function findMarkdownFiles(dir: string): string[] { const files: string[] = []; const entries = fs.readdirSync(dir, { withFileTypes: true }); @@ -23,13 +31,13 @@ function findMarkdownFiles(dir: string): string[] { function generateSearchIndex() { const docsDir = path.join(process.cwd(), "content/docs"); const files = findMarkdownFiles(docsDir); - const searchIndex = []; + const searchIndex: SearchIndexItem[] = []; for (const file of files) { const content = fs.readFileSync(file, "utf8"); const { data, content: markdownContent } = matter(content); const relativePath = path.relative(docsDir, file); - const urlPath = `/docs/${relativePath.replace(/\.mdx$/, "")}`; + const urlPath = `/docs/${relativePath.replace(MDX_EXTENSION_REGEX, "")}`; const excerpt = markdownContent .replace(/[#*`_~]/g, "") @@ -47,7 +55,7 @@ function generateSearchIndex() { return searchIndex; } -export async function GET() { +export function GET() { try { const searchIndex = generateSearchIndex(); return NextResponse.json(searchIndex); diff --git a/app/(website)/api/og/route.tsx b/app/(website)/api/og/route.tsx new file mode 100644 index 00000000..79c231e1 --- /dev/null +++ b/app/(website)/api/og/route.tsx @@ -0,0 +1,29 @@ +import { OgTemplate, loadFonts } from "@/components/og/og-template"; +import { ImageResponse } from "@vercel/og"; +import type { NextRequest } from "next/server"; + +export const runtime = "edge"; + +export async function GET(req: NextRequest) { + try { + const { searchParams } = new URL(req.url); + + const title = searchParams.get("title") || "EternalCode.pl"; + const subtitle = searchParams.get("subtitle") || "Open Source Solutions"; + const image = searchParams.get("image") || "https://eternalcode.pl/logo.svg"; + + const fonts = await loadFonts(); + + return new ImageResponse(, { + width: 1200, + height: 630, + fonts, + headers: { + "Cache-Control": "public, max-age=0, must-revalidate", + }, + }); + } catch (e) { + console.error("OG Image Generation Error:", e); + return new Response("Failed to generate OG image", { status: 500 }); + } +} diff --git a/app/(website)/api/sentry-test/route.ts b/app/(website)/api/sentry-test/route.ts new file mode 100644 index 00000000..9c6fee91 --- /dev/null +++ b/app/(website)/api/sentry-test/route.ts @@ -0,0 +1,5 @@ +export const dynamic = "force-dynamic"; + +export function GET() { + throw new Error("Sentry Test Error: Server-Side API Route"); +} diff --git a/app/(website)/author/[slug]/page.tsx b/app/(website)/author/[slug]/page.tsx new file mode 100644 index 00000000..591e1de8 --- /dev/null +++ b/app/(website)/author/[slug]/page.tsx @@ -0,0 +1,212 @@ +import type { Metadata } from "next"; +import Image from "next/image"; +import { notFound } from "next/navigation"; +import BlogPostCard from "@/components/blog/blog-post-card"; +import { SlideIn, StaggerContainer } from "@/components/ui/motion/motion-components"; +import { Pagination } from "@/components/ui/pagination"; +import { getAuthorBySlug, getBlogPostsByAuthor } from "@/lib/author"; +import { getImageUrl } from "@/lib/utils"; + +type AuthorPageProps = { + params: Promise<{ slug: string }>; + searchParams: Promise<{ page?: string }>; +}; + +export async function generateMetadata({ + params, +}: { + params: Promise<{ slug: string }>; +}): Promise { + const { slug } = await params; + const author = await getAuthorBySlug(slug); + + if (!author) { + return { + title: "Author Not Found | EternalCode.pl", + description: "This author does not exist on EternalCode.pl.", + }; + } + return { + title: `${author.name} – Author | EternalCode.pl`, + description: author.bio || `Read articles by ${author.name} on EternalCode.pl`, + openGraph: { + title: `${author.name} – Author | EternalCode.pl`, + description: author.bio || `Read articles by ${author.name} on EternalCode.pl`, + type: "profile", + url: `https://eternalcode.pl/author/${author.slug}`, + images: author.avatar?.url ? [getImageUrl(author.avatar.url)] : [], + firstName: author.name.split(" ")[0], + lastName: author.name.split(" ").slice(1).join(" ") || undefined, + username: author.slug, + }, + twitter: { + card: "summary", + title: `${author.name} – Author | EternalCode.pl`, + description: author.bio || `Read articles by ${author.name} on EternalCode.pl`, + images: author.avatar?.url ? [getImageUrl(author.avatar.url)] : [], + }, + alternates: { + canonical: `https://eternalcode.pl/author/${author.slug}`, + }, + }; +} + +export default async function AuthorPage({ params, searchParams }: AuthorPageProps) { + const { slug } = await params; + const { page } = await searchParams; + + if (!slug) { + notFound(); + } + + const author = await getAuthorBySlug(slug); + if (!author) { + notFound(); + } + + const posts = await getBlogPostsByAuthor(slug); + const ITEMS_PER_PAGE = 6; + const currentPage = Math.max(1, Number.parseInt(page || "1", 10)); + const totalPages = Math.ceil(posts.length / ITEMS_PER_PAGE); + const paginatedPosts = posts.slice( + (currentPage - 1) * ITEMS_PER_PAGE, + currentPage * ITEMS_PER_PAGE + ); + + return ( +
+ {/* Background Decor */} +
+ {/* Top Left - Teal/Cyan for "Author/Profile" */} +
+ {/* Middle Right - Purple for "Content" */} +
+ {/* Bottom Center - Blue */} +
+ +
+
+
+
+ +
+
+ {/* Author Profile Header */} + +
+ {!!author.avatar?.url && ( +
+
+
+ {author.name} +
+
+ )} +

+ {author.name} +

+ {!!author.bio && ( +

+ {author.bio} +

+ )} + {!!author.email && ( + + + + + {author.email} + + )} +
+ + + {/* Articles Section */} +
+ +

+ Articles by {author.name} +

+

+ {posts.length} {posts.length === 1 ? "article" : "articles"} published +

+
+ + {posts.length > 0 ? ( + <> + + {paginatedPosts.map((post, i) => ( + + + + ))} + + {totalPages > 1 && ( + + + + )} + + ) : ( + +
+
+ + + +
+

+ No articles yet +

+

+ {author.name} hasn't published any articles yet! +

+
+
+ )} +
+
+
+
+ ); +} diff --git a/app/(website)/blog/[slug]/opengraph-image.tsx b/app/(website)/blog/[slug]/opengraph-image.tsx new file mode 100644 index 00000000..fc82b183 --- /dev/null +++ b/app/(website)/blog/[slug]/opengraph-image.tsx @@ -0,0 +1,57 @@ +import { OgTemplate, loadFonts } from "@/components/og/og-template"; +import { getBlogPost } from "@/lib/blog"; +import { getImageUrl } from "@/lib/utils"; +import { ImageResponse } from "next/og"; + +export const runtime = "nodejs"; // Must be nodejs because Payload CMS client requires it +export const alt = "EternalCode Blog Post"; +export const size = { + width: 1200, + height: 630, +}; + +export const contentType = "image/png"; + +export default async function Image({ params }: { params: Promise<{ slug: string }> }) { + const { slug } = await params; + const post = await getBlogPost(slug); + + const fonts = await loadFonts(); + + if (!post) { + return new ImageResponse(, { + ...size, + fonts, + }); + } + + // If a featured image exists, we could use it as a background or just the image. + // Strategy: Use the featured image as the background with an overlay + text, + // OR if it's a dedicated OG image designed by humans, maybe just show it? + // User asked for "Dynamic data (title, thumbnail)". + // Let's go with: Featured Image as Background (blurred/overlay) + Title over it, + // OR if the user provides a specific "ogImage" field in CMS (not seen in type), we use that. + // The 'featuredImage' seems to be the main visual. + // Let's try utilizing the shared template's 'backgroundImage' prop if available. + + const featuredImageUrl = post.featuredImage?.url + ? getImageUrl(post.featuredImage.url) + : undefined; + + return new ImageResponse( + , + { + ...size, + fonts, + // Cache for a long time since blog posts don't change often + headers: { + "Cache-Control": "public, max-age=0, must-revalidate", + }, + } + ); +} diff --git a/app/blog/[slug]/page.tsx b/app/(website)/blog/[slug]/page.tsx similarity index 69% rename from app/blog/[slug]/page.tsx rename to app/(website)/blog/[slug]/page.tsx index 9ec2dd81..145581e2 100644 --- a/app/blog/[slug]/page.tsx +++ b/app/(website)/blog/[slug]/page.tsx @@ -2,16 +2,15 @@ import type { Metadata } from "next"; import Image from "next/image"; import Link from "next/link"; import { notFound } from "next/navigation"; - -import { SlideIn } from "@/components/ui/motion/MotionComponents"; -import BlogPostContent from "@/components/blog/BlogPostContent"; -import { generateOgImageUrl } from "@/lib/og-utils"; -import { getBlogPost, type StrapiTag } from "@/lib/strapi"; +import BlogPostContent from "@/components/blog/blog-post-content"; +import { SlideIn } from "@/components/ui/motion/motion-components"; +import { type CMSTag, getBlogPost } from "@/lib/blog"; +import { getImageUrl } from "@/lib/utils"; export const dynamic = "force-dynamic"; export const revalidate = 5; -export async function generateStaticParams() { +export function generateStaticParams() { try { return []; } catch (error) { @@ -20,18 +19,11 @@ export async function generateStaticParams() { } } -function getImageUrl(url: string) { - if (!url) return ""; - if (url.startsWith("http")) return url; - const base = process.env.NEXT_PUBLIC_ETERNALCODE_STRAPI_URL || ""; - return `${base}${url}`; -} - -function getTagsArray(tags: StrapiTag[] | { data: StrapiTag[] } | undefined): StrapiTag[] { - if (!tags) return []; - if (Array.isArray(tags)) return tags; - if ("data" in tags && Array.isArray(tags.data)) return tags.data; - return []; +function getTagsArray(tags: CMSTag[] | undefined): CMSTag[] { + if (!tags) { + return []; + } + return tags; } export async function generateMetadata({ @@ -49,19 +41,12 @@ export async function generateMetadata({ }; } - const ogImageUrl = post.featuredImage?.url - ? getImageUrl(post.featuredImage.url) - : generateOgImageUrl({ - title: post.title, - subtitle: post.excerpt, - }); - const tagsArr = getTagsArray(post.tags); return { title: `${post.title} | EternalCode.pl`, description: post.excerpt, - keywords: tagsArr.map((tag: StrapiTag) => tag.name) || [], + keywords: tagsArr.map((tag: CMSTag) => tag.name) || [], authors: [{ name: post.author?.name || "EternalCode Team" }], openGraph: { type: "article", @@ -70,18 +55,10 @@ export async function generateMetadata({ siteName: "EternalCode.pl", title: post.title, description: post.excerpt, - images: [ - { - url: ogImageUrl, - width: 1200, - height: 630, - alt: post.title, - }, - ], publishedTime: post.publishedAt, modifiedTime: post.updatedAt, authors: [post.author?.name || "EternalCode Team"], - tags: tagsArr.map((tag: StrapiTag) => tag.name) || [], + tags: tagsArr.map((tag: CMSTag) => tag.name) || [], }, twitter: { card: "summary_large_image", @@ -89,7 +66,6 @@ export async function generateMetadata({ creator: "@eternalcode", title: post.title, description: post.excerpt, - images: [ogImageUrl], }, alternates: { canonical: `https://eternalcode.pl/blog/${post.slug}`, @@ -117,27 +93,27 @@ export default async function BlogPostPage({ params }: { params: Promise<{ slug: return (
{/* Hero Section */} - +
-

+

{post.title}

-

+

{post.excerpt}

-
- {post.author?.slug && ( +
+ {!!post.author?.slug && ( - {post.author.avatar && ( + {!!post.author.avatar && ( {post.author.name} )} By {post.author.name} @@ -151,7 +127,7 @@ export default async function BlogPostPage({ params }: { params: Promise<{ slug: day: "numeric", })} - {post.readingTime && ( + {!!post.readingTime && ( <> {post.readingTime} min read @@ -160,10 +136,10 @@ export default async function BlogPostPage({ params }: { params: Promise<{ slug:
{tagsArr.length > 0 && (
- {tagsArr.map((tag: StrapiTag) => ( + {tagsArr.map((tag: CMSTag) => ( {tag.name} @@ -174,7 +150,7 @@ export default async function BlogPostPage({ params }: { params: Promise<{ slug: {/* Blog Content */} - +
diff --git a/app/(website)/blog/page.tsx b/app/(website)/blog/page.tsx new file mode 100644 index 00000000..2e02b675 --- /dev/null +++ b/app/(website)/blog/page.tsx @@ -0,0 +1,81 @@ +import type { Metadata } from "next"; + +import BlogListing from "@/components/blog/blog-listing"; +import { SlideIn } from "@/components/ui/motion/motion-components"; + +import { getBlogPosts } from "@/lib/blog"; + +export const dynamic = "force-dynamic"; +export const revalidate = 60; // Increased revalidation time slightly + +export const metadata: Metadata = { + title: "Blog | EternalCode.pl", + description: "Discover the latest insights, tutorials, and articles from our team of experts.", + keywords: ["blog", "articles", "tutorials", "insights", "technology", "programming"], + openGraph: { + type: "website", + locale: "en_US", + url: "https://eternalcode.pl/blog", + siteName: "EternalCode.pl", + title: "Blog | EternalCode.pl", + description: "Discover the latest insights, tutorials, and articles from our team of experts.", + images: [ + { + url: "https://eternalcode.pl/api/og?title=Blog&subtitle=Latest insights and tutorials", + width: 1200, + height: 630, + alt: "EternalCode Blog", + }, + ], + }, + twitter: { + card: "summary_large_image", + site: "@eternalcode", + creator: "@eternalcode", + title: "Blog | EternalCode.pl", + description: "Discover the latest insights, tutorials, and articles from our team of experts.", + images: ["https://eternalcode.pl/api/og?title=Blog&subtitle=Latest insights and tutorials"], + }, + alternates: { + canonical: "https://eternalcode.pl/blog", + }, +}; + +export default async function BlogPage() { + const posts = await getBlogPosts(); + + return ( +
+ {/* Background Decor */} +
+ {/* Top Right - Indigo/Purple */} +
+ {/* Middle Left - Rose/Pink */} +
+ {/* Bottom Center - Blue */} +
+ +
+
+
+
+ +
+
+ {/* Header */} + +

+ Insights & Thoughts +

+

+ Exploring the frontiers of technology, coding, and digital innovation. +

+
+ + {/* Content Listing */} + +
+
+
+ ); +} diff --git a/app/(website)/builds/page.tsx b/app/(website)/builds/page.tsx new file mode 100644 index 00000000..3fda93b4 --- /dev/null +++ b/app/(website)/builds/page.tsx @@ -0,0 +1,150 @@ +"use client"; + +import { + fetchDevBuilds, + fetchStableBuilds, + PROJECTS, + type Project, +} from "@/app/(website)/api/builds/builds"; +import { BuildControls } from "@/components/builds/build-controls"; +import { BuildHeader } from "@/components/builds/build-header"; +import type { Build } from "@/components/builds/build-row"; +import { BuildTable } from "@/components/builds/build-table"; +import { FacadePattern } from "@/components/ui/facade-pattern"; +import { Loader2 } from "lucide-react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { Suspense, useEffect, useState } from "react"; + +function BuildExplorerContent() { + const searchParams = useSearchParams(); + const router = useRouter(); + + const projectIdParam = searchParams.get("project"); + const initialProject = PROJECTS.find((p) => p.id === projectIdParam) || PROJECTS[0]; + + const [activeProject, setActiveProject] = useState(initialProject); + const [activeTab, setActiveTab] = useState<"STABLE" | "DEV">("STABLE"); + const [builds, setBuilds] = useState([]); + const [loading, setLoading] = useState(true); + const [lastDownloadedId, setLastDownloadedId] = useState(null); + + useEffect(() => { + const foundProject = PROJECTS.find((p) => p.id === projectIdParam); + if (foundProject && foundProject.id !== activeProject.id) { + setActiveProject(foundProject); + } + }, [projectIdParam, activeProject.id]); + + useEffect(() => { + const stored = localStorage.getItem(`last_download_${activeProject.id}`); + setLastDownloadedId(stored); + }, [activeProject.id]); + + const handleProjectChange = (projectId: string) => { + const project = PROJECTS.find((p) => p.id === projectId); + if (!project) { + return; + } + + setActiveProject(project); + const params = new URLSearchParams(searchParams.toString()); + params.set("project", project.id); + router.push(`/builds?${params.toString()}`); + }; + + useEffect(() => { + async function fetchData() { + setLoading(true); + setBuilds([]); // Clear previous builds + try { + if (activeTab === "STABLE") { + const data = await fetchStableBuilds(activeProject); + setBuilds( + data.map((version) => ({ + id: version.id, + name: version.name, + type: "STABLE", + date: version.date_published, + downloadUrl: version.files?.[0]?.url || "", + version: version.version_number, + })) + ); + } else { + const runs = await fetchDevBuilds(activeProject); + setBuilds( + runs.map((run) => { + const displayTitle = run.display_title; + const artifactName = run.found_artifact?.name || `${activeProject.name} Dev Build`; + + return { + id: run.id.toString(), + name: displayTitle || run.name || `Run #${run.id}`, + type: "DEV", + date: run.created_at, + downloadUrl: `https://nightly.link/${ + activeProject.githubRepo + }/actions/runs/${run.id}/${encodeURIComponent(artifactName)}.zip`, + commit: run.head_sha.substring(0, 7), + runUrl: run.html_url, + }; + }) + ); + } + } catch (e) { + console.error("Failed to load builds", e); + } finally { + setLoading(false); + } + } + fetchData(); + }, [activeTab, activeProject]); + + return ( +
+ {/* Background Decor */} +
+
+
+
+ +
+ +
+ + + + + { + localStorage.setItem(`last_download_${activeProject.id}`, id); + setLastDownloadedId(id); + }} + project={activeProject} + /> +
+
+ ); +} + +export default function BuildExplorerPage() { + return ( + + +
+ } + > + + + ); +} diff --git a/app/(website)/contribute/contribute-view.tsx b/app/(website)/contribute/contribute-view.tsx new file mode 100644 index 00000000..a9c39e85 --- /dev/null +++ b/app/(website)/contribute/contribute-view.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { motion } from "framer-motion"; +import { ContributeHero } from "@/components/contribute/contribute-hero"; +import { ContributionCard } from "@/components/contribute/contribution-card"; +import { ContributionEmptyState } from "@/components/contribute/contribution-empty-state"; +import { ContributionHint } from "@/components/contribute/contribution-hint"; + +const containerVariants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.1, + }, + }, +}; + +export type ContributionCardData = { + id?: string | null; + title: string; + description: string; + icon?: string | null; + actionText: string; + href: string; + color: string; +}; + +export default function ContributeView({ cards }: { cards: ContributionCardData[] }) { + return ( +
+ {/* Background decoration */} +
+
+
+
+ + + +
+ {cards.length > 0 ? ( + + {cards.map((card, index) => ( + + ))} + + ) : ( + + )} + + +
+
+ ); +} diff --git a/app/(website)/contribute/page.tsx b/app/(website)/contribute/page.tsx new file mode 100644 index 00000000..59433c66 --- /dev/null +++ b/app/(website)/contribute/page.tsx @@ -0,0 +1,39 @@ +import { getPayload } from "payload"; +import config from "@/payload.config"; +import type { ContributePage as ContributePageType } from "@/payload-types-generated"; +import ContributeView from "./contribute-view"; + +export const metadata = { + title: "Contribute | EternalCode", + description: + "Join the EternalCode community. Contribute code, support us financially, or help with documentation and support.", +}; + +type CardData = NonNullable[number]; + +export default async function ContributePage() { + let cards: CardData[] = []; + + try { + const payload = await getPayload({ config }); + const globalData = await payload.findGlobal({ + slug: "contribute-page", + }); + + if (globalData.cards && globalData.cards.length > 0) { + cards = globalData.cards.map((card) => ({ + id: card.id, + title: card.title, + description: card.description, + icon: card.icon || "HelpCircle", // Fallback icon if missing + actionText: card.actionText, + href: card.href, + color: card.color, + })); + } + } catch (e) { + console.error("Failed to fetch contribution cards from Payload CMS", e); + } + + return ; +} diff --git a/app/docs/[...slug]/page.tsx b/app/(website)/docs/[...slug]/page.tsx similarity index 79% rename from app/docs/[...slug]/page.tsx rename to app/(website)/docs/[...slug]/page.tsx index bb41e80b..90217296 100644 --- a/app/docs/[...slug]/page.tsx +++ b/app/(website)/docs/[...slug]/page.tsx @@ -1,44 +1,43 @@ import fs from "node:fs/promises"; import path from "node:path"; -import { Suspense } from "react"; - import matter from "gray-matter"; import type { Metadata } from "next"; import { notFound } from "next/navigation"; import { MDXRemote } from "next-mdx-remote/rsc"; +import { Suspense } from "react"; -import { DocsHeader } from "@/components/docs/content/DocsHeader"; -import { DocsNavigation } from "@/components/docs/content/DocsNavigation"; -import { EditOnGitHub } from "@/components/docs/content/EditOnGitHub"; -import { ErrorBoundary } from "@/components/docs/content/ErrorBoundary"; -import { ReadingTime } from "@/components/docs/content/ReadingTime"; -import { components, mdxOptions } from "@/components/mdx/mdx-components"; +import { DocsHeader } from "@/components/docs/content/docs-header"; +import { DocsNavigation } from "@/components/docs/content/docs-navigation"; +import { EditOnGitHub } from "@/components/docs/content/edit-on-github"; +import { ErrorBoundary } from "@/components/docs/content/error-boundary"; +import { ReadingTime } from "@/components/docs/content/reading-time"; +import { components, mdxOptions } from "@/components/ui/mdx/mdx-components"; import { docsStructure } from "@/lib/sidebar-structure"; -interface DocMeta { +type DocMeta = { title: string; description?: string; lastModified?: string; author?: string; icon?: string; [key: string]: string | undefined; -} +}; -interface Doc { +type Doc = { meta: DocMeta; content: string; -} +}; -interface DocNavigation { +type DocNavigation = { prev: { title: string; path: string } | null; next: { title: string; path: string } | null; -} +}; -interface DocStructureItem { +type DocStructureItem = { title: string; path: string; children?: DocStructureItem[]; -} +}; function getFlatDocs(): { title: string; path: string }[] { function flattenDocs(structure: DocStructureItem[]): { title: string; path: string }[] { @@ -91,11 +90,11 @@ function getDocNavigation(currentPath: string): DocNavigation { }; } -interface Props { +type Props = { params: Promise<{ slug: string[]; }>; -} +}; export async function generateMetadata({ params }: Props): Promise { const resolvedParams = await params; @@ -133,13 +132,13 @@ function LoadingFallback() {
{skeletonKeys.map((key) => ( -
+
))}
); } -export async function generateStaticParams() { +export function generateStaticParams() { const flatDocs = getFlatDocs(); return flatDocs.map((doc) => ({ slug: doc.path.replace("/docs/", "").split("/"), @@ -149,7 +148,9 @@ export async function generateStaticParams() { export default async function DocPage({ params }: Props) { const resolvedParams = await params; const doc = await getDocBySlug(resolvedParams.slug); - if (!doc) notFound(); + if (!doc) { + notFound(); + } const currentPath = `/docs/${resolvedParams.slug.join("/")}`; const { prev, next } = getDocNavigation(currentPath); @@ -158,30 +159,35 @@ export default async function DocPage({ params }: Props) { return (
-
+
} + category={category} + description={doc.meta.description} + icon={doc.meta.icon} + title={doc.meta.title} /> -
+
}> - +
- +
); } diff --git a/app/docs/layout.tsx b/app/(website)/docs/layout.tsx similarity index 58% rename from app/docs/layout.tsx rename to app/(website)/docs/layout.tsx index 7f5abe0a..9f3703e4 100644 --- a/app/docs/layout.tsx +++ b/app/(website)/docs/layout.tsx @@ -1,8 +1,7 @@ import type { Metadata } from "next"; import { Poppins } from "next/font/google"; - -import SidebarWrapper from "@/components/docs/sidebar/SidebarWrapper"; import type { ReactNode } from "react"; +import SidebarWrapper from "@/components/docs/sidebar/sidebar-wrapper"; const poppins = Poppins({ weight: "500", @@ -35,12 +34,21 @@ export const metadata: Metadata = { }, }; +import { FacadePattern } from "@/components/ui/facade-pattern"; + export default function DocsLayout({ children }: { children: ReactNode }) { return (
-
+ {/* Background Decor */} +
+
+
+ +
+ +
diff --git a/app/docs/page.tsx b/app/(website)/docs/page.tsx similarity index 100% rename from app/docs/page.tsx rename to app/(website)/docs/page.tsx diff --git a/app/globals.css b/app/(website)/globals.css similarity index 81% rename from app/globals.css rename to app/(website)/globals.css index 21894b92..a5bfe79a 100644 --- a/app/globals.css +++ b/app/(website)/globals.css @@ -1,6 +1,6 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import "tailwindcss"; +@import "../../node_modules/@south-paw/typeface-minecraft/index.css" layer(base); +@import "./prism-tomorrow.css" layer(base); @layer base { :root { @@ -77,36 +77,44 @@ } } +/* Moved generic block here to fix descending specificity and Biome errors */ +pre, +code, +.prose pre, +.prose code { + font-family: + "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace !important; +} + code[class*="language-"], pre[class*="language-"] { color: var(--prism-text, #24292e) !important; background-color: var(--prism-bg, #ffffff) !important; text-shadow: none !important; - font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace !important; + font-family: + Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace !important; } -code[class*="language-"]>span:not([class]) { +code[class*="language-"] > span:not([class]) { color: var(--prism-text, #24292e) !important; } -:not(pre)>code[class*="language-"], +:not(pre) > code[class*="language-"], pre[class*="language-"] { background: var(--prism-bg, #ffffff) !important; } -@import '../node_modules/@south-paw/typeface-minecraft/index.css' layer(base); -@import './prism-tomorrow.css' layer(base); -@import 'tailwindcss'; - @custom-variant dark (&:is(.dark *)); @theme { --font-monospace: - 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace; + "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace; --background-image-gradient-radial: radial-gradient(var(--tw-gradient-stops)); - --background-image-gradient-conic: conic-gradient(from 180deg at 50% 50%, - var(--tw-gradient-stops)); + --background-image-gradient-conic: conic-gradient( + from 180deg at 50% 50%, + var(--tw-gradient-stops) + ); --color-gray-50: #f7f8fa; --color-gray-100: #eef1f5; @@ -153,7 +161,6 @@ pre[class*="language-"] { color utility to any element that depends on these defaults. */ @layer base { - *, ::after, ::before, @@ -164,7 +171,6 @@ pre[class*="language-"] { } @utility scrollbar-hide { - /* Hide scrollbar for Chrome, Safari and Opera */ &::-webkit-scrollbar { display: none; @@ -177,9 +183,15 @@ pre[class*="language-"] { /* Firefox */ } +@utility optimize-visibility { + content-visibility: auto; + contain-intrinsic-size: 100px; +} + code[class*="language-"], pre[class*="language-"] { - font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace !important; + font-family: + "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace !important; font-size: 1em; text-align: left; white-space: pre; @@ -200,7 +212,7 @@ pre[class*="language-"] { border-radius: 0.375rem; } -:not(pre)>code[class*="language-"] { +:not(pre) > code[class*="language-"] { padding: 0.1em; border-radius: 0.25rem; white-space: normal; @@ -212,7 +224,7 @@ pre[class*="language-"].line-numbers { counter-reset: linenumber; } -pre[class*="language-"].line-numbers>code { +pre[class*="language-"].line-numbers > code { position: relative; white-space: inherit; } @@ -229,12 +241,12 @@ pre[class*="language-"].line-numbers>code { user-select: none; } -.line-numbers-rows>span { +.line-numbers-rows > span { display: block; counter-increment: linenumber; } -.line-numbers-rows>span:before { +.line-numbers-rows > span:before { content: counter(linenumber); color: #999; display: block; @@ -277,6 +289,22 @@ pre[class*="language-"].line-numbers>code { } } +@keyframes slide-up { + from { + opacity: 0; + transform: translateY(20px); + } + + to { + opacity: 1; + transform: none; + } +} + +.animate-slide-up { + animation: slide-up 0.6s cubic-bezier(0.2, 0.8, 0.2, 1) both; +} + .animate-fadein { animation: fadein 0.4s cubic-bezier(0.4, 2, 0.6, 1) both; } @@ -314,20 +342,14 @@ pre { font-weight: 600; } -pre, -code, -.prose pre, -.prose code { - font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace !important; -} - code[class*="language-"], pre[class*="language-"] { text-shadow: none !important; - font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace !important; + font-family: + Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace !important; } -:not(pre)>code[class*="language-"], +:not(pre) > code[class*="language-"], pre[class*="language-"] { color: var(--prism-text) !important; -} \ No newline at end of file +} diff --git a/app/layout.tsx b/app/(website)/layout.tsx similarity index 76% rename from app/layout.tsx rename to app/(website)/layout.tsx index 444086c5..8255de6f 100644 --- a/app/layout.tsx +++ b/app/(website)/layout.tsx @@ -2,16 +2,15 @@ import type { Metadata, Viewport } from "next"; import { Poppins } from "next/font/google"; import NextTopLoader from "nextjs-toploader"; import "./globals.css"; +import "lenis/dist/lenis.css"; import type React from "react"; -import { Analytics } from "@/components/Analytics"; +import { Analytics } from "@/components/analytics"; import "./prism-languages"; -import { CookieConsentModal } from "@/components/CookieConsentModal"; -import { CookiePreferencesMenu } from "@/components/CookiePreferencesMenu"; -import Footer from "@/components/footer/Footer"; -import Navbar from "@/components/hero/Navbar"; -import { SpeedInsights } from "@/components/SpeedInsights"; -import { generateOgImageUrl } from "@/lib/og-utils"; +import { CookieConsentModal } from "@/components/cookie-consent-modal"; +import Footer from "@/components/footer/footer"; +import Navbar from "@/components/hero/navbar"; +import { SpeedInsights } from "@/components/speed-insights"; import { Providers } from "./providers"; @@ -38,11 +37,6 @@ export const viewport: Viewport = { ], }; -const defaultOgImageUrl = generateOgImageUrl({ - title: "EternalCode.pl", - subtitle: "We are a team creating open source projects!", -}); - export const metadata: Metadata = { metadataBase: new URL("https://eternalcode.pl"), title: "EternalCode.pl | Home", @@ -65,14 +59,6 @@ export const metadata: Metadata = { title: "EternalCode.pl | We are a team creating open source projects!", description: "EternalCode.pl delivers open source solutions with a focus on quality, performance, and innovation.", - images: [ - { - url: defaultOgImageUrl, - width: 1200, - height: 630, - alt: "EternalCode.pl", - }, - ], }, twitter: { card: "summary_large_image", @@ -81,7 +67,6 @@ export const metadata: Metadata = { title: "EternalCode.pl | We are a team creating open source projects!", description: "EternalCode.pl delivers open source solutions with a focus on quality, performance, and innovation.", - images: [defaultOgImageUrl], }, robots: { index: true, @@ -105,21 +90,21 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - +
@@ -131,7 +116,6 @@ export default function RootLayout({