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 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 (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}`,
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
2
next-env.d.ts
vendored
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user