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) => {
{post.author} {
-
- -
+
+ +
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}

- -
- {post.author} -
- 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} -

- -
- {post.author} -
- - By {post.author} - - - {post.readTime} - -
-
-
- - ))} -
-
- -
-

All posts

-
- {allPosts.map((post) => ( - -
-

- {post.title} -

-

- {post.description} -

- -
- {post.author} -
- - 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}