diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 407be2a..c8cc210 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,9 @@ jobs: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 - run: bun install --frozen-lockfile + - name: Dependency vulnerability scan + run: bun audit + continue-on-error: true - run: bun run type-check - run: bun run lint - run: bun run test diff --git a/next.config.ts b/next.config.ts index eec398f..40fed3a 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,5 +1,34 @@ import type { NextConfig } from "next"; +const securityHeaders = [ + { key: "X-DNS-Prefetch-Control", value: "on" }, + { + key: "Strict-Transport-Security", + value: "max-age=63072000; includeSubDomains; preload", + }, + { key: "X-Frame-Options", value: "DENY" }, + { key: "X-Content-Type-Options", value: "nosniff" }, + { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }, + { + key: "Permissions-Policy", + value: "camera=(), microphone=(), geolocation=(), browsing-topics=()", + }, + { + key: "Content-Security-Policy", + value: [ + "default-src 'self'", + "script-src 'self' 'unsafe-inline'", + "style-src 'self' 'unsafe-inline'", + "img-src 'self' data: https:", + "font-src 'self' https://fonts.gstatic.com", + "connect-src 'self'", + "frame-ancestors 'none'", + "base-uri 'self'", + "form-action 'self'", + ].join("; "), + }, +]; + const nextConfig: NextConfig = { experimental: { serverActions: { @@ -14,6 +43,14 @@ const nextConfig: NextConfig = { }, ], }, + async headers() { + return [ + { + source: "/(.*)", + headers: securityHeaders, + }, + ]; + }, }; export default nextConfig; diff --git a/src/app/blog/[slug]/page.tsx b/src/app/blog/[slug]/page.tsx index 3b2192c..9b235a0 100644 --- a/src/app/blog/[slug]/page.tsx +++ b/src/app/blog/[slug]/page.tsx @@ -7,6 +7,8 @@ import { PostContent } from "@/components/blog/post-content"; import { TableOfContents } from "@/components/blog/table-of-contents"; import { Badge } from "@/components/ui/badge"; import { buildMetadata } from "@/lib/metadata"; +import { publicEnv } from "@/lib/env"; +import { siteConfig } from "@/lib/site"; import { extractTableOfContents } from "@/lib/markdown"; import { estimateReadingTime, formatDate } from "@/lib/utils"; import { @@ -55,8 +57,33 @@ export default async function BlogPostPage({ params }: BlogPostPageProps) { const [adjacentPosts] = await Promise.all([getAdjacentPublishedPosts(slug)]); const tableOfContents = extractTableOfContents(post.content); + const blogPostingSchema = { + "@context": "https://schema.org", + "@type": "BlogPosting", + headline: post.title, + description: post.excerpt, + datePublished: post.createdAt, + dateModified: post.updatedAt, + url: `${publicEnv.NEXT_PUBLIC_APP_URL}/blog/${post.slug}`, + author: { + "@type": "Person", + name: siteConfig.name, + url: publicEnv.NEXT_PUBLIC_APP_URL, + }, + ...(post.coverImage + ? { image: { "@type": "ImageObject", url: post.coverImage } } + : {}), + }; + return ( -
+ Critical error +
++ The error was handled safely. You can try refreshing the page or + clicking the button below to recover. +
+ +