feat/intl-multi-lang #10
42
.dockerignore
Normal file
42
.dockerignore
Normal 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
6
.env.example
Normal file
@@ -0,0 +1,6 @@
|
||||
# Production site URL (REQUIRED for SEO)
|
||||
NEXT_PUBLIC_SITE_URL=https://yourdomain.com
|
||||
|
||||
# Environment
|
||||
NODE_ENV=production
|
||||
PORT=3030
|
||||
@@ -76,7 +76,7 @@ export default async function BlogPostPage({ params }: { params: Promise<{ slug:
|
||||
|
||||
const relatedPosts = await getRelatedPosts(slugPath)
|
||||
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 (
|
||||
<>
|
||||
|
||||
@@ -11,7 +11,7 @@ export const metadata: Metadata = {
|
||||
default: 'Terminal Blog - Build. Write. Share.',
|
||||
},
|
||||
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' }],
|
||||
keywords: ['blog', 'dezvoltare web', 'nextjs', 'react', 'typescript', 'terminal'],
|
||||
openGraph: {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import rehypeSanitize from 'rehype-sanitize'
|
||||
import rehypeRaw from 'rehype-raw'
|
||||
import { OptimizedImage } from './OptimizedImage'
|
||||
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}`}>
|
||||
<ReactMarkdown
|
||||
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={{
|
||||
img: ({ node, src, alt, title, ...props }) => {
|
||||
if (!src || typeof src !== 'string') return null
|
||||
|
||||
@@ -12,7 +12,7 @@ export function BreadcrumbsSchema({ items }: { items: BreadcrumbSchemaItem[] })
|
||||
'@type': 'ListItem',
|
||||
position: item.position,
|
||||
name: item.name,
|
||||
item: `http://localhost:3000${item.item}`,
|
||||
item: `${process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3030'}${item.item}`,
|
||||
})),
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ services:
|
||||
# Docker monitors the application and marks it unhealthy if checks fail
|
||||
# If container is unhealthy, restart policy will trigger a restart
|
||||
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
|
||||
timeout: 10s # Wait up to 10 seconds for response
|
||||
retries: 3 # Mark unhealthy after 3 consecutive failures
|
||||
|
||||
@@ -15,6 +15,15 @@ export function sanitizePath(inputPath: string): string {
|
||||
if (normalized.includes('..') || path.isAbsolute(normalized)) {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -39,8 +39,9 @@ async function copyAndRewritePath(node: ImageNode, options: Options): Promise<vo
|
||||
|
||||
const sourcePath = path.resolve(contentPostDir, urlWithoutParams)
|
||||
|
||||
if (sourcePath.includes('..') && !sourcePath.startsWith(path.join(process.cwd(), contentDir))) {
|
||||
throw new Error(`Invalid image path: ${node.url} (path traversal detected)`)
|
||||
const allowedBasePath = path.join(process.cwd(), contentDir)
|
||||
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)
|
||||
|
||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <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
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
Reference in New Issue
Block a user