diff --git a/.dockerignore b/.dockerignore
index 0795297..8a5c499 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,4 +1,5 @@
Dockerfile
+build/
.dockerignore
node_modules
npm-debug.log
diff --git a/Dockerfile b/Dockerfile
index 3a86f8a..51b753a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,55 +1,20 @@
-# Use Node.js 18 Alpine as base image
-FROM node:18-alpine AS base
-
-# Install dependencies only when needed
-FROM base AS deps
-# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
-RUN apk add --no-cache libc6-compat
-WORKDIR /app
-
-# Install dependencies (include devDependencies so build works)
-COPY package.json package-lock.json* ./
-RUN npm ci
-
-# Rebuild the source code only when needed
-FROM base AS builder
+# Build stage
+FROM node:20-alpine AS builder
WORKDIR /app
-COPY --from=deps /app/node_modules ./node_modules
COPY . .
-
-# Next.js collects completely anonymous telemetry data about general usage.
-# Learn more here: https://nextjs.org/telemetry
-ENV NEXT_TELEMETRY_DISABLED 1
-
+RUN npm install
RUN npm run build
-# Production image, copy all the files and run next
-FROM base AS runner
-WORKDIR /app
-
-ENV NODE_ENV production
-ENV NEXT_TELEMETRY_DISABLED 1
-
-RUN addgroup --system --gid 1001 nodejs
-RUN adduser --system --uid 1001 nextjs
-
-COPY --from=builder /app/public ./public
-
-# Set the correct permission for prerender cache
-RUN mkdir .next
-RUN chown nextjs:nodejs .next
-
-# Automatically leverage output traces to reduce image size
-# https://nextjs.org/docs/advanced-features/output-file-tracing
-COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
-COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
-
-USER nextjs
+# Production stage
+FROM nginx:alpine
+WORKDIR /usr/share/nginx/html
+RUN mkdir blog
+COPY --from=builder /app/build ./blog
-EXPOSE 3000
+# Replace default.conf with our own
+RUN rm -rf /etc/nginx/conf.d/*
+COPY default.conf /etc/nginx/conf.d/default.conf
-ENV PORT 3000
-ENV HOSTNAME "0.0.0.0"
+EXPOSE 80
+CMD ["nginx", "-g", "daemon off;"]
-# server.js is created by next build from the standalone output
-CMD ["node", "server.js"]
diff --git a/default.conf b/default.conf
new file mode 100644
index 0000000..5309d82
--- /dev/null
+++ b/default.conf
@@ -0,0 +1,55 @@
+server {
+ listen 80;
+ listen [::]:80;
+ server_name localhost;
+
+ #access_log /var/log/nginx/host.access.log main;
+
+ location / {
+ root /usr/share/nginx/html;
+ index index.html index.htm;
+ }
+
+ location /blog {
+ alias /usr/share/nginx/html/blog;
+ try_files $uri $uri/ /blog/index.html;
+
+ # Handle static assets (images, CSS, JS)
+ location ~* \.(png|jpg|jpeg|gif|svg|ico|css|js)$ {
+ expires 1y;
+ add_header Cache-Control "public, immutable";
+ }
+ }
+
+ #error_page 404 /404.html;
+
+ # redirect server error pages to the static page /50x.html
+ #
+ error_page 500 502 503 504 /50x.html;
+ location = /50x.html {
+ root /usr/share/nginx/html;
+ }
+
+ # proxy the PHP scripts to Apache listening on 127.0.0.1:80
+ #
+ #location ~ \.php$ {
+ # proxy_pass http://127.0.0.1;
+ #}
+
+ # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
+ #
+ #location ~ \.php$ {
+ # root html;
+ # fastcgi_pass 127.0.0.1:9000;
+ # fastcgi_index index.php;
+ # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
+ # include fastcgi_params;
+ #}
+
+ # deny access to .htaccess files, if Apache's document root
+ # concurs with nginx's one
+ #
+ #location ~ /\.ht {
+ # deny all;
+ #}
+}
\ No newline at end of file
diff --git a/nexlayer-update.yaml b/nexlayer-update.yaml
index dc87d8e..f151a05 100644
--- a/nexlayer-update.yaml
+++ b/nexlayer-update.yaml
@@ -5,7 +5,7 @@ application:
- name: blog
image: ghcr.io/nexlayer/nexlayer-blog:latest
path: "/"
- servicePorts: [3000]
+ servicePorts: [80]
vars:
NODE_ENV: "production"
NEXT_TELEMETRY_DISABLED: "1"
diff --git a/nexlayer.yaml b/nexlayer.yaml
index 5b32743..afb8f36 100644
--- a/nexlayer.yaml
+++ b/nexlayer.yaml
@@ -3,8 +3,8 @@ application:
pods:
- name: blog
image: ghcr.io/nexlayer/nexlayer-blog:latest
- path: "/blog"
- servicePorts: [3000]
+ path: "/"
+ servicePorts: [80]
vars:
NODE_ENV: "production"
NEXT_TELEMETRY_DISABLED: "1"
diff --git a/next.config.ts b/next.config.ts
index a67efdf..16d0b27 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -2,16 +2,12 @@ import { NextConfig } from 'next'
import createMDX from '@next/mdx'
const nextConfig: NextConfig = {
pageExtensions: ['js', 'jsx', 'mdx', 'ts', 'tsx'],
- output: 'standalone',
- async redirects() {
- return [
- {
- source: '/',
- destination: '/blog',
- permanent: true,
- },
- ]
- },
+ output: 'export', // Enable static export
+ distDir: 'build', // Output directory
+ basePath: '/blog',
+ images: {
+ unoptimized: true
+ }
}
const withMDX = createMDX({
extension: /\.mdx?$/,
diff --git a/src/app/[slug]/page.tsx b/src/app/[slug]/page.tsx
index b66b892..5372409 100644
--- a/src/app/[slug]/page.tsx
+++ b/src/app/[slug]/page.tsx
@@ -1,14 +1,25 @@
import React from "react";
import Image from "next/image";
import Link from "next/link";
-import BlogMdxContent from "../../components/BlogMdxContent";
+import SimpleMdxRenderer from "@/components/SimpleMdxRenderer";
import fs from "fs";
import { ArrowLeft } from "lucide-react";
import path from "path";
import matter from "gray-matter";
import NotFound from "@/app/not-found";
-import { serialize } from "next-mdx-remote/serialize";
+// Generate static params for all blog posts
+export async function generateStaticParams() {
+ const contentDir = path.join(process.cwd(), "src", "content");
+ try {
+ const files = fs.readdirSync(contentDir).filter((f) => f.endsWith(".mdx"));
+ return files.map((filename) => ({
+ slug: filename.replace(/\.mdx$/, ""),
+ }));
+ } catch (err) {
+ return [];
+ }
+}
interface BlogPostProps {
params: Promise<{ slug: string }>;
@@ -20,7 +31,7 @@ async function getPostBySlug(slug: string) {
if (!fs.existsSync(filePath)) return null;
const source = fs.readFileSync(filePath, "utf-8");
const { data, content } = matter(source);
- const mdxSource = await serialize(content, { scope: data });
+
return {
title: data.title || slug,
description: data.description || "",
@@ -28,7 +39,6 @@ async function getPostBySlug(slug: string) {
avatar: data.avatar || "/placeholder.svg",
readTime: data.readTime || "",
date: data.date || "",
- mdxSource,
content,
};
}
@@ -41,6 +51,8 @@ const BlogPost = async ({ params }: BlogPostProps) => {
return ;
}
+
+
return (
@@ -61,7 +73,7 @@ const BlogPost = async ({ params }: BlogPostProps) => {
{
-
-
-
+
+
+
diff --git a/src/app/blog/[slug]/page.tsx b/src/app/blog/[slug]/page.tsx
deleted file mode 100644
index a19614e..0000000
--- a/src/app/blog/[slug]/page.tsx
+++ /dev/null
@@ -1,88 +0,0 @@
-import React from "react";
-import Image from "next/image";
-import Link from "next/link";
-import BlogMdxContent from "../../../components/BlogMdxContent";
-import BlogContentClient from "../../../components/BlogContentClient";
-import fs from "fs";
-import { ArrowLeft } from "lucide-react";
-import path from "path";
-import matter from "gray-matter";
-import NotFound from "@/app/not-found";
-
-import { serialize } from "next-mdx-remote/serialize";
-
-interface BlogPostProps {
- params: Promise<{ slug: string }>;
-}
-
-async function getPostBySlug(slug: string) {
- const contentDir = path.join(process.cwd(), "src", "content");
- const filePath = path.join(contentDir, `${slug}.mdx`);
- if (!fs.existsSync(filePath)) return null;
- const source = fs.readFileSync(filePath, "utf-8");
- const { data, content } = matter(source);
- const mdxSource = await serialize(content, { scope: data });
- return {
- title: data.title || slug,
- description: data.description || "",
- author: data.author || "Unknown",
- avatar: data.avatar || "/placeholder.svg",
- readTime: data.readTime || "",
- date: data.date || "",
- mdxSource,
- content,
- };
-}
-
-const BlogPost = async ({ params }: BlogPostProps) => {
- const { slug } = await params;
- const post = await getPostBySlug(slug);
-
- if (!post) {
- return ;
- }
-
- return (
-
-
-
-
-
- BACK TO THE MAIN BLOG
-
-
-
{post.date}
-
-
{post.title}
-
-
{post.description}
-
-
-
-
- Posted By {post.author}
- {post.readTime}
-
-
-
-
- } />
-
-
-
-
- );
-};
-
-export default BlogPost;
diff --git a/src/app/blog/page.tsx b/src/app/blog/page.tsx
deleted file mode 100644
index 78c6914..0000000
--- a/src/app/blog/page.tsx
+++ /dev/null
@@ -1,133 +0,0 @@
-import React from "react";
-import Link from "next/link";
-import Image from "next/image";
-import fs from "fs";
-import path from "path";
-import matter from "gray-matter";
-// import LogoIcon from "../../../public/logo-icon.svg";
-
-function getAllPosts() {
- const contentDir = path.join(process.cwd(), "src", "content");
- const files = fs.readdirSync(contentDir).filter((f) => f.endsWith(".mdx"));
- return files.map((filename) => {
- const filePath = path.join(contentDir, filename);
- const source = fs.readFileSync(filePath, "utf-8");
- const { data } = matter(source);
-
- return {
- title: data.title || filename.replace(/\.mdx$/, ""),
- description: data.description || "",
- author: data.author || "Unknown",
- avatar: data.avatar || "/placeholder.svg",
- readTime: data.readTime || "",
- slug: filename.replace(/\.mdx$/, ""),
- date: data.date || null,
- };
- });
-}
-
-export default function BlogPage() {
- const posts = getAllPosts();
- const featuredPosts = posts.slice(0, 4);
- function parseCustomDate(dateStr: string | null): number {
- if (!dateStr) return 0;
- const months: { [key: string]: string } = {
- JANUARY: "01", FEBRUARY: "02", MARCH: "03", APRIL: "04", MAY: "05", JUNE: "06",
- JULY: "07", AUGUST: "08", SEPTEMBER: "09", OCTOBER: "10", NOVEMBER: "11", DECEMBER: "12"
- };
- const parts: string[] = dateStr.toUpperCase().replace(/,/g, "").split(" ");
- if (parts.length === 3 && months[parts[0] as keyof typeof months]) {
- return new Date(`${parts[2]}-${months[parts[0] as keyof typeof months]}-${parts[1].padStart(2, "0")}`).getTime();
- }
- return new Date(dateStr).getTime();
- }
- const allPosts = [...posts].sort((a, b) => {
- return parseCustomDate(b.date) - parseCustomDate(a.date);
- });
- return (
-
-
-
-
-
Blog
-
- Compiled notes from the team
-
-
-
-
-
Featured
-
- {featuredPosts.map((post) => (
-
-
-
- {post.title}
-
-
- {post.description}
-
-
-
-
-
-
- By {post.author}
-
-
- {post.readTime}
-
-
-
-
-
- ))}
-
-
-
-
-
All posts
-
- {allPosts.map((post) => (
-
-
-
- {post.title}
-
-
- {post.description}
-
-
-
-
-
-
- By {post.author}
-
-
- {post.readTime}
-
-
-
-
-
- ))}
-
-
-
-
-
- );
-}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index ee269f2..18cf09b 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,6 +1,6 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
-import Navbar from "../components/Navbar";
+import StaticNavigation from "../components/StaticNavbar";
import Footer from "../components/Footer";
import "./globals.css";
@@ -97,7 +97,7 @@ export default function RootLayout({
-
+
{children}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index cfe2282..65bc1ef 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -4,7 +4,6 @@ import Image from "next/image";
import fs from "fs";
import path from "path";
import matter from "gray-matter";
-// import LogoIcon from "../../../public/logo-icon.svg";
function getAllPosts() {
const contentDir = path.join(process.cwd(), "src", "content");
@@ -70,7 +69,7 @@ export default function BlogPage() {
All posts
{allPosts.map((post) => (
-
+
{post.title}
@@ -106,7 +105,7 @@ export default function BlogPage() {
);
-}
+}
\ No newline at end of file
diff --git a/src/app/robots.txt/route.ts b/src/app/robots.txt/route.ts
deleted file mode 100644
index ae87053..0000000
--- a/src/app/robots.txt/route.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { NextResponse } from "next/server";
-
-export async function GET() {
- const robots = `User-agent: *\nAllow: /\nSitemap: ${
- process.env.NEXT_PUBLIC_SITE_URL || "https://nexlayer.com"
- }/sitemap.xml`;
- return new NextResponse(robots, {
- headers: {
- "Content-Type": "text/plain",
- },
- });
-}
diff --git a/src/app/sitemap.xml/route.ts b/src/app/sitemap.xml/route.ts
deleted file mode 100644
index 07c0818..0000000
--- a/src/app/sitemap.xml/route.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { NextResponse } from "next/server";
-import fs from "fs";
-import path from "path";
-
-export async function GET() {
- const contentDir = path.join(process.cwd(), "src", "content");
- let slugs: string[] = [];
- try {
- slugs = fs
- .readdirSync(contentDir)
- .filter((file) => file.endsWith(".mdx"))
- .map((file) => file.replace(/\.mdx$/, ""));
- } catch (err) {
- slugs = [];
- }
-
- const urls = ["/", "/blog", ...slugs.map((slug) => `/blog/${slug}`)];
-
- const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://nexlayer.com";
- const sitemap = `\n\n${urls
- .map(
- (url) =>
- ` ${baseUrl}${url}monthly1.0`
- )
- .join("\n")}\n`;
-
- return new NextResponse(sitemap, {
- headers: {
- "Content-Type": "application/xml",
- },
- });
-}
diff --git a/src/components/BlogCodeBlock.tsx b/src/components/BlogCodeBlock.tsx
index 53d3b84..e0c355b 100644
--- a/src/components/BlogCodeBlock.tsx
+++ b/src/components/BlogCodeBlock.tsx
@@ -1,4 +1,3 @@
-"use client";
import React from "react";
interface BlogCodeBlockProps {
diff --git a/src/components/BlogMdxContent.tsx b/src/components/BlogMdxContent.tsx
index cb3be79..e6373ed 100644
--- a/src/components/BlogMdxContent.tsx
+++ b/src/components/BlogMdxContent.tsx
@@ -1,4 +1,3 @@
-"use client";
import React from "react";
import MdxRenderer from "./MdxRenderer";
import type { MDXRemoteSerializeResult } from "next-mdx-remote";
diff --git a/src/components/MdxRenderer.tsx b/src/components/MdxRenderer.tsx
index 34e906a..384ad1c 100644
--- a/src/components/MdxRenderer.tsx
+++ b/src/components/MdxRenderer.tsx
@@ -1,4 +1,3 @@
-"use client";
import React from "react";
import { MDXRemote } from "next-mdx-remote";
import type { MDXRemoteSerializeResult } from "next-mdx-remote";
diff --git a/src/components/SimpleMdxRenderer.tsx b/src/components/SimpleMdxRenderer.tsx
new file mode 100644
index 0000000..ed3f807
--- /dev/null
+++ b/src/components/SimpleMdxRenderer.tsx
@@ -0,0 +1,159 @@
+import React from "react";
+import BlogCodeBlock from "./BlogCodeBlock";
+
+interface SimpleMdxRendererProps {
+ content: string;
+}
+
+const SimpleMdxRenderer: React.FC = ({ content }) => {
+ // Enhanced MDX-like rendering for static export
+ // This handles more MDX features while remaining static-compatible
+
+ const renderContent = (text: string) => {
+ // Split content into lines and process each one
+ const lines = text.split('\n');
+ const elements: React.ReactNode[] = [];
+ let currentList: React.ReactNode[] = [];
+ let inList = false;
+
+ lines.forEach((line, index) => {
+ const trimmedLine = line.trim();
+
+ // Handle headers
+ if (trimmedLine.startsWith('## ')) {
+ if (inList && currentList.length > 0) {
+ elements.push();
+ currentList = [];
+ inList = false;
+ }
+ elements.push({trimmedLine.substring(3)}
);
+ return;
+ }
+
+ if (trimmedLine.startsWith('### ')) {
+ if (inList && currentList.length > 0) {
+ elements.push();
+ currentList = [];
+ inList = false;
+ }
+ elements.push({trimmedLine.substring(4)}
);
+ return;
+ }
+
+ // Handle lists
+ if (trimmedLine.startsWith('- **') || trimmedLine.startsWith('* **')) {
+ if (!inList) {
+ inList = true;
+ }
+
+ // Extract bold text and description
+ const match = trimmedLine.match(/^[-*]\s\*\*(.*?)\*\*:\s*(.*)/);
+ if (match) {
+ const [, boldText, description] = match;
+ currentList.push(
+
+ {boldText}: {description}
+
+ );
+ } else {
+ // Handle regular list items
+ const listText = trimmedLine.replace(/^[-*]\s/, '');
+ currentList.push(
+ {listText}
+ );
+ }
+ return;
+ }
+
+ // Handle regular list items
+ if (trimmedLine.startsWith('- ') || trimmedLine.startsWith('* ')) {
+ if (!inList) {
+ inList = true;
+ }
+ const listText = trimmedLine.replace(/^[-*]\s/, '');
+ currentList.push(
+ {listText}
+ );
+ return;
+ }
+
+ // Handle code blocks
+ if (trimmedLine.startsWith('```')) {
+ if (inList && currentList.length > 0) {
+ elements.push();
+ currentList = [];
+ inList = false;
+ }
+
+ // Start of code block
+ const language = trimmedLine.replace('```', '').trim();
+ const codeLines: string[] = [];
+ let i = index + 1;
+
+ // Collect code lines until we find the closing ```
+ while (i < lines.length && !lines[i].trim().startsWith('```')) {
+ codeLines.push(lines[i]);
+ i++;
+ }
+
+ if (i < lines.length) {
+ // Skip the closing ``` line in the main loop
+ index = i;
+ }
+
+ const codeContent = codeLines.join('\n');
+ elements.push(
+
+ {codeContent}
+
+ );
+ return;
+ }
+
+ // Handle paragraphs (non-empty lines that aren't headers, list items, or code blocks)
+ if (trimmedLine && !trimmedLine.startsWith('#') && !trimmedLine.startsWith('-') && !trimmedLine.startsWith('*') && !trimmedLine.startsWith('```')) {
+ if (inList && currentList.length > 0) {
+ elements.push();
+ currentList = [];
+ inList = false;
+ }
+
+ // Process bold text and inline code within paragraphs
+ let processedText = trimmedLine
+ .replace(/\*\*(.*?)\*\*/g, '$1')
+ .replace(/`([^`]+)`/g, '$1');
+
+ elements.push(
+
+ );
+ return;
+ }
+
+ // Handle empty lines
+ if (!trimmedLine) {
+ if (inList && currentList.length > 0) {
+ elements.push();
+ currentList = [];
+ inList = false;
+ }
+ elements.push(
);
+ }
+ });
+
+ // Handle any remaining list
+ if (inList && currentList.length > 0) {
+ elements.push();
+ }
+
+ return elements;
+ };
+
+ return (
+
+ {renderContent(content)}
+
+ );
+};
+
+export default SimpleMdxRenderer;
diff --git a/src/components/Navbar.tsx b/src/components/StaticNavbar.tsx
similarity index 50%
rename from src/components/Navbar.tsx
rename to src/components/StaticNavbar.tsx
index a10edbb..d69642d 100644
--- a/src/components/Navbar.tsx
+++ b/src/components/StaticNavbar.tsx
@@ -1,30 +1,13 @@
-"use client"
-
-import { useState, useEffect } from "react"
-import { NexlayerLogo } from "../components/NexlayerLogo"
-
-export const Navigation = () => {
- const [isScrolled, setIsScrolled] = useState(false)
-
- useEffect(() => {
- const handleScroll = () => {
- setIsScrolled(window.scrollY > 0)
- }
-
- window.addEventListener("scroll", handleScroll)
- return () => window.removeEventListener("scroll", handleScroll)
- }, [])
+import React from "react";
+import { NexlayerLogo } from "./NexlayerLogo";
+export const StaticNavigation = () => {
return (
-