feat/production-improvements #8

Merged
raresj merged 2 commits from feat/production-improvements into master 2025-12-02 10:42:12 +00:00
10 changed files with 77 additions and 8 deletions
Showing only changes of commit f383b86b4d - Show all commits

42
.dockerignore Normal file
View File

@@ -0,0 +1,42 @@
# Git
.git
.github
.gitignore
# Dependencies
node_modules
# Next.js
.next
out
# Environment
.env*
!.env.example
# Logs
*.log
npm-debug.log*
logs/
# Documentation
*.md
!README.md
specs/
# IDE
.vscode
.idea
# OS
.DS_Store
Thumbs.db
# Testing
.coverage
.nyc_output
# Misc
*.swp
*.swo
*~

6
.env.example Normal file
View File

@@ -0,0 +1,6 @@
# Production site URL (REQUIRED for SEO)
NEXT_PUBLIC_SITE_URL=https://yourdomain.com
# Environment
NODE_ENV=production
PORT=3030

View File

@@ -76,7 +76,7 @@ export default async function BlogPostPage({ params }: { params: Promise<{ slug:
const relatedPosts = await getRelatedPosts(slugPath) const relatedPosts = await getRelatedPosts(slugPath)
const headings = extractHeadings(post.content) const headings = extractHeadings(post.content)
const fullUrl = `https://yourdomain.com/blog/${slugPath}` const fullUrl = `${process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3030'}/blog/${slugPath}`
return ( return (
<> <>

View File

@@ -11,7 +11,7 @@ export const metadata: Metadata = {
default: 'Terminal Blog - Build. Write. Share.', default: 'Terminal Blog - Build. Write. Share.',
}, },
description: 'Explorează idei despre dezvoltare, design și tehnologie', description: 'Explorează idei despre dezvoltare, design și tehnologie',
metadataBase: new URL('http://localhost:3000'), metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3030'),
authors: [{ name: 'Terminal User' }], authors: [{ name: 'Terminal User' }],
keywords: ['blog', 'dezvoltare web', 'nextjs', 'react', 'typescript', 'terminal'], keywords: ['blog', 'dezvoltare web', 'nextjs', 'react', 'typescript', 'terminal'],
openGraph: { openGraph: {

View File

@@ -2,6 +2,7 @@
import ReactMarkdown from 'react-markdown' import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm' import remarkGfm from 'remark-gfm'
import rehypeSanitize from 'rehype-sanitize'
import rehypeRaw from 'rehype-raw' import rehypeRaw from 'rehype-raw'
import { OptimizedImage } from './OptimizedImage' import { OptimizedImage } from './OptimizedImage'
import { CodeBlock } from './code-block' import { CodeBlock } from './code-block'
@@ -17,7 +18,17 @@ export default function MarkdownRenderer({ content, className = '' }: MarkdownRe
<div className={`prose prose-invert prose-zinc max-w-none ${className}`}> <div className={`prose prose-invert prose-zinc max-w-none ${className}`}>
<ReactMarkdown <ReactMarkdown
remarkPlugins={[remarkGfm]} remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw]} rehypePlugins={[rehypeRaw, [rehypeSanitize, {
tagNames: ['p', 'a', 'img', 'code', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'ul', 'ol', 'li', 'blockquote', 'table', 'thead', 'tbody', 'tr', 'th', 'td',
'strong', 'em', 'del', 'br', 'hr', 'div', 'span'],
attributes: {
a: ['href', 'rel', 'target'],
img: ['src', 'alt', 'title', 'width', 'height'],
code: ['className'],
'*': ['className', 'id']
}
}]]}
components={{ components={{
img: ({ node, src, alt, title, ...props }) => { img: ({ node, src, alt, title, ...props }) => {
if (!src || typeof src !== 'string') return null if (!src || typeof src !== 'string') return null

View File

@@ -12,7 +12,7 @@ export function BreadcrumbsSchema({ items }: { items: BreadcrumbSchemaItem[] })
'@type': 'ListItem', '@type': 'ListItem',
position: item.position, position: item.position,
name: item.name, name: item.name,
item: `http://localhost:3000${item.item}`, item: `${process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3030'}${item.item}`,
})), })),
} }

View File

@@ -59,7 +59,7 @@ services:
# Docker monitors the application and marks it unhealthy if checks fail # Docker monitors the application and marks it unhealthy if checks fail
# If container is unhealthy, restart policy will trigger a restart # If container is unhealthy, restart policy will trigger a restart
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3030/", "||", "exit", "1"] test: ["CMD-SHELL", "curl -f http://localhost:3030/ || exit 1"]
interval: 30s # Check every 30 seconds interval: 30s # Check every 30 seconds
timeout: 10s # Wait up to 10 seconds for response timeout: 10s # Wait up to 10 seconds for response
retries: 3 # Mark unhealthy after 3 consecutive failures retries: 3 # Mark unhealthy after 3 consecutive failures

View File

@@ -15,6 +15,15 @@ export function sanitizePath(inputPath: string): string {
if (normalized.includes('..') || path.isAbsolute(normalized)) { if (normalized.includes('..') || path.isAbsolute(normalized)) {
throw new Error('Invalid path') throw new Error('Invalid path')
} }
// CRITICAL: Verify resolved path stays within content directory
const resolvedPath = path.resolve(POSTS_PATH, normalized)
const allowedBasePath = path.resolve(POSTS_PATH)
if (!resolvedPath.startsWith(allowedBasePath)) {
throw new Error('Path traversal attempt detected')
}
return normalized return normalized
} }

View File

@@ -39,8 +39,9 @@ async function copyAndRewritePath(node: ImageNode, options: Options): Promise<vo
const sourcePath = path.resolve(contentPostDir, urlWithoutParams) const sourcePath = path.resolve(contentPostDir, urlWithoutParams)
if (sourcePath.includes('..') && !sourcePath.startsWith(path.join(process.cwd(), contentDir))) { const allowedBasePath = path.join(process.cwd(), contentDir)
throw new Error(`Invalid image path: ${node.url} (path traversal detected)`) if (!sourcePath.startsWith(allowedBasePath)) {
throw new Error(`Invalid image path outside content directory: ${node.url}`)
} }
const relativeToContent = path.relative(path.join(process.cwd(), contentDir), sourcePath) const relativeToContent = path.relative(path.join(process.cwd(), contentDir), sourcePath)

2
next-env.d.ts vendored
View File

@@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts"; import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.