Implement comprehensive homepage improvements and SEO optimization
Major homepage and SEO enhancements based on optimization document: **Homepage Content Updates:** - Updated H1 titles with SEO-optimized text for both RO/EN - Enhanced hero descriptions with targeted keywords - Improved feature descriptions for better clarity - Updated daily verse section with keyword-rich titles - Added new footer description with SEO focus **SEO Implementation:** - Added dynamic metadata generation with locale-specific SEO - Implemented Open Graph tags for social media sharing - Added Twitter Card metadata for enhanced sharing - Integrated Schema.org JSON-LD structured data - Set up hreflang tags for international SEO - Added canonical URLs to prevent duplicate content - Included targeted keywords for both languages **Technical Improvements:** - Migrated from Docker to PM2 deployment - Removed Docker files and updated deployment scripts - Updated README with PM2 instructions - Fixed console log cleanup for production - Added proper favicon with Next.js app directory - Increased memory limit to 4GB for better performance - Updated port configuration to 0.0.0.0:3010 - Set Romanian (/ro) as default locale with proper redirects **Translation Updates:** - Enhanced Romanian translations with SEO-optimized content - Updated English translations with matching SEO improvements - Added new 'seo' namespace for metadata translations 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
56
Dockerfile
56
Dockerfile
@@ -1,56 +0,0 @@
|
||||
FROM node:20-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 based on the preferred package manager
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm ci
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM base 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
|
||||
# Uncomment the following line in case you want to disable telemetry during the build.
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
# Uncomment the following line in case you want to disable telemetry during runtime.
|
||||
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
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV PORT=3000
|
||||
|
||||
# server.js is created by next build from the standalone output
|
||||
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
|
||||
CMD HOSTNAME="0.0.0.0" node server.js
|
||||
41
README.md
41
README.md
@@ -54,12 +54,12 @@ O aplicație web completă pentru studiul Bibliei cu capabilități de chat AI
|
||||
- **AI**: Azure OpenAI API cu fallback la Ollama
|
||||
- **Security**: JWT, bcrypt, rate limiting, input validation
|
||||
- **Testing**: Jest, React Testing Library, TypeScript
|
||||
- **DevOps**: Docker, Docker Compose, Nginx, SSL support
|
||||
- **DevOps**: PM2, Nginx, SSL support
|
||||
- **Performance**: Caching, indexing, optimization scripts
|
||||
|
||||
## Instalare Rapidă
|
||||
|
||||
### Folosind Docker (Recomandat)
|
||||
### Folosind PM2 (Recomandat)
|
||||
|
||||
1. Clonează repository-ul:
|
||||
```bash
|
||||
@@ -83,18 +83,25 @@ JWT_SECRET=your-secure-jwt-secret
|
||||
NEXTAUTH_SECRET=your-secure-nextauth-secret
|
||||
```
|
||||
|
||||
4. Pornește aplicația:
|
||||
4. Instalează dependențele și construiește aplicația:
|
||||
```bash
|
||||
docker-compose up -d
|
||||
npm ci
|
||||
npm run build
|
||||
```
|
||||
|
||||
5. Rulează migrațiile și importă datele biblice:
|
||||
```bash
|
||||
docker-compose exec app npx prisma migrate deploy
|
||||
docker-compose exec app npm run import-bible
|
||||
npx prisma migrate deploy
|
||||
npx prisma generate
|
||||
npm run import-bible
|
||||
```
|
||||
|
||||
6. Accesează aplicația la: http://localhost:3000
|
||||
6. Pornește aplicația cu PM2:
|
||||
```bash
|
||||
pm2 start ecosystem.config.js --env production
|
||||
```
|
||||
|
||||
7. Accesează aplicația la: http://localhost:3000
|
||||
|
||||
### Instalare Manuală
|
||||
|
||||
@@ -149,7 +156,7 @@ npm run dev
|
||||
│ └── db.ts # Conexiunea la baza de date
|
||||
├── prisma/ # Schema și migrații Prisma
|
||||
├── scripts/ # Scripturi de utilitate
|
||||
└── docker/ # Configurații Docker
|
||||
└── ecosystem.config.js # Configurație PM2
|
||||
```
|
||||
|
||||
## Configurare AI
|
||||
@@ -171,12 +178,20 @@ Pentru rularea locală de modele AI:
|
||||
|
||||
## Deployment în Producție
|
||||
|
||||
### Folosind Docker
|
||||
### Folosind PM2
|
||||
|
||||
1. Copiază `.env.example` la `.env.production` și configurează-l
|
||||
2. Construiește și pornește serviciile:
|
||||
1. Copiază `.env.example` la `.env` și configurează-l pentru producție
|
||||
2. Rulează scriptul de deployment:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
./deploy.sh
|
||||
```
|
||||
|
||||
Sau manual:
|
||||
```bash
|
||||
npm ci
|
||||
npm run build
|
||||
pm2 restart ghidul-biblic || pm2 start ecosystem.config.js --env production
|
||||
pm2 save
|
||||
```
|
||||
|
||||
### Configurare SSL
|
||||
@@ -194,7 +209,7 @@ sudo certbot --nginx -d yourdomain.com
|
||||
## Monitorizare
|
||||
|
||||
- **Health Check**: `/api/health`
|
||||
- **Logs**: `docker-compose logs -f app`
|
||||
- **Logs**: `pm2 logs ghidul-biblic`
|
||||
- **Metrici**: Implementate prin endpoint-uri dedicate
|
||||
|
||||
## Contribuții
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import '../globals.css'
|
||||
import type { Metadata } from 'next'
|
||||
import { NextIntlClientProvider } from 'next-intl'
|
||||
import { getMessages } from 'next-intl/server'
|
||||
import { getMessages, getTranslations } from 'next-intl/server'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { MuiThemeProvider } from '@/components/providers/theme-provider'
|
||||
import { AuthProvider } from '@/components/auth/auth-provider'
|
||||
@@ -9,9 +9,66 @@ import { Navigation } from '@/components/layout/navigation'
|
||||
import FloatingChat from '@/components/chat/floating-chat'
|
||||
import { merriweather, lato } from '@/lib/fonts'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Ghid Biblic - Biblical Guide',
|
||||
description: 'A comprehensive Bible study application with AI chat capabilities',
|
||||
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
||||
const { locale } = await params
|
||||
const t = await getTranslations({ locale, namespace: 'seo' })
|
||||
|
||||
const currentUrl = locale === 'ro' ? 'https://ghidulbiblic.ro/ro/' : 'https://ghidulbiblic.ro/en/'
|
||||
const alternateUrl = locale === 'ro' ? 'https://ghidulbiblic.ro/en/' : 'https://ghidulbiblic.ro/ro/'
|
||||
|
||||
return {
|
||||
title: t('title'),
|
||||
description: t('description'),
|
||||
keywords: t('keywords'),
|
||||
alternates: {
|
||||
canonical: currentUrl,
|
||||
languages: {
|
||||
'ro': 'https://ghidulbiblic.ro/ro/',
|
||||
'en': 'https://ghidulbiblic.ro/en/',
|
||||
'x-default': 'https://ghidulbiblic.ro/'
|
||||
}
|
||||
},
|
||||
openGraph: {
|
||||
title: t('ogTitle'),
|
||||
description: t('ogDescription'),
|
||||
url: currentUrl,
|
||||
siteName: locale === 'ro' ? 'Ghid Biblic' : 'Biblical Guide',
|
||||
locale: locale,
|
||||
type: 'website',
|
||||
images: [
|
||||
{
|
||||
url: `https://ghidulbiblic.ro/og-image-${locale}.jpg`,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: t('ogTitle'),
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
site: '@ghidbiblic',
|
||||
title: t('twitterTitle'),
|
||||
description: t('twitterDescription'),
|
||||
images: [`https://ghidulbiblic.ro/og-image-${locale}.jpg`],
|
||||
},
|
||||
other: {
|
||||
'application/ld+json': JSON.stringify({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "MobileApplication",
|
||||
"name": locale === 'ro' ? "Ghid Biblic" : "Biblical Guide",
|
||||
"url": "https://ghidulbiblic.ro",
|
||||
"description": t('description'),
|
||||
"applicationCategory": "EducationApplication",
|
||||
"operatingSystem": "iOS, Android, Web",
|
||||
"inLanguage": [locale],
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"price": "0",
|
||||
"priceCurrency": "USD"
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
|
||||
@@ -44,6 +44,7 @@ export default function Home() {
|
||||
const theme = useTheme()
|
||||
const router = useRouter()
|
||||
const t = useTranslations('home')
|
||||
const tSeo = useTranslations('seo')
|
||||
const locale = useLocale()
|
||||
const [userCount, setUserCount] = useState(2847)
|
||||
const [expandedFaq, setExpandedFaq] = useState<string | false>(false)
|
||||
@@ -595,7 +596,7 @@ export default function Home() {
|
||||
{t('footer.brand')}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="grey.400" sx={{ maxWidth: 300 }}>
|
||||
{t('footer.description')}
|
||||
{tSeo('footer')}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--foreground-rgb: 0, 0, 0;
|
||||
|
||||
32
app/icon.tsx
Normal file
32
app/icon.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { ImageResponse } from 'next/og'
|
||||
|
||||
export const size = {
|
||||
width: 32,
|
||||
height: 32,
|
||||
}
|
||||
export const contentType = 'image/png'
|
||||
|
||||
export default function Icon() {
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
fontSize: 24,
|
||||
background: '#333',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: 'white',
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
B
|
||||
</div>
|
||||
),
|
||||
{
|
||||
...size,
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -38,7 +38,6 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
||||
|
||||
// Check if token is expired before making request
|
||||
if (isTokenExpired(token)) {
|
||||
console.log('Token expired in refreshUser, clearing auth state')
|
||||
localStorage.removeItem('authToken')
|
||||
setUser(null)
|
||||
setIsLoading(false)
|
||||
@@ -62,14 +61,6 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
||||
const data = await response.json()
|
||||
setUser(data.user)
|
||||
} else {
|
||||
// Token is invalid or expired, get error details
|
||||
try {
|
||||
const errorData = await response.json()
|
||||
console.log('Server returned 401 error:', errorData)
|
||||
} catch (e) {
|
||||
console.log('Server returned 401 without JSON body, status:', response.status)
|
||||
}
|
||||
console.log('Token expired or invalid, clearing auth state')
|
||||
localStorage.removeItem('authToken')
|
||||
setUser(null)
|
||||
}
|
||||
@@ -83,81 +74,18 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
||||
}
|
||||
}
|
||||
|
||||
// Debug database schema
|
||||
const debugSchema = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/debug/schema')
|
||||
const debug = await response.json()
|
||||
console.log('Database schema info:', debug)
|
||||
} catch (e) {
|
||||
console.log('Schema debug failed:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// Debug user lookup
|
||||
const debugUser = async (userId: string) => {
|
||||
try {
|
||||
const response = await fetch('/api/debug/user', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ userId })
|
||||
})
|
||||
const debug = await response.json()
|
||||
console.log('User debug info:', debug)
|
||||
} catch (e) {
|
||||
console.log('User debug failed:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// Debug token validation
|
||||
const debugToken = async (token: string) => {
|
||||
try {
|
||||
const response = await fetch('/api/debug/token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token })
|
||||
})
|
||||
const debug = await response.json()
|
||||
console.log('Token debug info:', debug)
|
||||
|
||||
// Log more details about the token payload
|
||||
if (debug.payload) {
|
||||
console.log('Token payload:', debug.payload)
|
||||
|
||||
// Debug user lookup
|
||||
if (debug.payload.userId) {
|
||||
debugUser(debug.payload.userId)
|
||||
}
|
||||
}
|
||||
if (debug.verificationResult) {
|
||||
console.log('Verification result:', debug.verificationResult)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Token debug failed:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// Clear expired tokens and sync state immediately on mount
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const token = localStorage.getItem('authToken')
|
||||
console.log('Auth mount check - token exists:', !!token)
|
||||
if (token) {
|
||||
console.log('Token preview:', token.substring(0, 50) + '...')
|
||||
|
||||
// Debug the database schema and token on server side
|
||||
debugSchema()
|
||||
debugToken(token)
|
||||
|
||||
const expired = isTokenExpired(token)
|
||||
console.log('Token expired:', expired)
|
||||
if (expired) {
|
||||
console.log('Clearing expired token and user state on mount')
|
||||
localStorage.removeItem('authToken')
|
||||
setUser(null)
|
||||
}
|
||||
} else if (user) {
|
||||
console.log('No token but user exists in store, clearing user state')
|
||||
setUser(null)
|
||||
}
|
||||
clearExpiredToken()
|
||||
@@ -168,23 +96,19 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
||||
useEffect(() => {
|
||||
if (!initialized && typeof window !== 'undefined') {
|
||||
const token = localStorage.getItem('authToken')
|
||||
console.log('Initialization flow - token exists:', !!token)
|
||||
|
||||
if (token) {
|
||||
// Check if token is expired before making request
|
||||
if (isTokenExpired(token)) {
|
||||
console.log('Token expired during initialization, clearing auth state')
|
||||
localStorage.removeItem('authToken')
|
||||
setUser(null)
|
||||
setIsLoading(false)
|
||||
} else {
|
||||
console.log('Token is valid, calling refreshUser()')
|
||||
// Token appears valid, try to refresh user data
|
||||
// refreshUser will handle server-side validation failures
|
||||
refreshUser()
|
||||
}
|
||||
} else {
|
||||
console.log('No token found, clearing user state')
|
||||
// No token, clear any stale user data
|
||||
setUser(null)
|
||||
setIsLoading(false)
|
||||
|
||||
@@ -138,7 +138,6 @@ export default function FloatingChat() {
|
||||
const checkAuthStatus = useCallback(async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('authToken')
|
||||
console.log('Chat - checkAuthStatus: token from localStorage:', token ? 'present' : 'null')
|
||||
if (token) {
|
||||
// Verify token with the server
|
||||
const response = await fetch('/api/auth/me', {
|
||||
@@ -146,20 +145,16 @@ export default function FloatingChat() {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
console.log('Chat - auth verification response:', response.ok)
|
||||
|
||||
if (response.ok) {
|
||||
setAuthToken(token)
|
||||
setIsAuthenticated(true)
|
||||
console.log('Chat - Auth state set: authenticated')
|
||||
} else {
|
||||
localStorage.removeItem('authToken')
|
||||
setIsAuthenticated(false)
|
||||
setAuthToken(null)
|
||||
console.log('Chat - Auth verification failed, cleared state')
|
||||
}
|
||||
} else {
|
||||
console.log('Chat - No token in localStorage')
|
||||
setIsAuthenticated(false)
|
||||
setAuthToken(null)
|
||||
}
|
||||
|
||||
55
deploy.sh
55
deploy.sh
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Deploy script for Biblical Guide production server
|
||||
# Fetches latest code from production branch and deploys
|
||||
# Fetches latest code from production branch and deploys with PM2
|
||||
|
||||
set -e
|
||||
|
||||
@@ -11,6 +11,7 @@ echo "🚀 Starting deployment..."
|
||||
REPO_URL="https://git.noru1.ro/andrei/ghidul-biblic.git"
|
||||
BRANCH="production"
|
||||
APP_NAME="ghidul-biblic"
|
||||
PORT="3010"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
@@ -60,10 +61,6 @@ fi
|
||||
|
||||
print_success "Environment variables validated"
|
||||
|
||||
# Stop existing containers
|
||||
print_status "Stopping existing containers..."
|
||||
docker compose down || true
|
||||
|
||||
# Fetch latest code from production branch
|
||||
print_status "Fetching latest code from $BRANCH branch..."
|
||||
git fetch origin $BRANCH
|
||||
@@ -76,40 +73,54 @@ CURRENT_COMMIT=$(git rev-parse --short HEAD)
|
||||
COMMIT_MSG=$(git log -1 --pretty=format:"%s")
|
||||
print_status "Current commit: $CURRENT_COMMIT - $COMMIT_MSG"
|
||||
|
||||
# Build and start the application
|
||||
print_status "Building and starting application..."
|
||||
docker compose up --build -d
|
||||
# Install dependencies if package.json changed
|
||||
if git diff --name-only HEAD~1 HEAD | grep -q "package.json\|package-lock.json"; then
|
||||
print_status "Dependencies changed, installing..."
|
||||
npm ci
|
||||
print_success "Dependencies installed"
|
||||
fi
|
||||
|
||||
# Build the application
|
||||
print_status "Building application..."
|
||||
npm run build
|
||||
|
||||
print_success "Application built successfully"
|
||||
|
||||
# Restart with PM2
|
||||
print_status "Restarting application with PM2..."
|
||||
pm2 restart $APP_NAME || pm2 start ecosystem.config.js --env production
|
||||
|
||||
# Save PM2 configuration
|
||||
pm2 save
|
||||
|
||||
print_success "Application restarted with PM2"
|
||||
|
||||
# Wait for application to be ready
|
||||
print_status "Waiting for application to start..."
|
||||
sleep 10
|
||||
sleep 5
|
||||
|
||||
# Health check
|
||||
print_status "Performing health check..."
|
||||
for i in {1..30}; do
|
||||
if curl -f http://localhost:3010/api/health >/dev/null 2>&1; then
|
||||
print_success "Application is healthy and running on port 3010"
|
||||
if curl -f http://localhost:$PORT/api/health >/dev/null 2>&1; then
|
||||
print_success "Application is healthy and running on port $PORT"
|
||||
break
|
||||
fi
|
||||
|
||||
if [ $i -eq 30 ]; then
|
||||
print_error "Health check failed after 30 attempts"
|
||||
print_status "Showing container logs:"
|
||||
docker compose logs --tail=50
|
||||
print_status "Showing PM2 logs:"
|
||||
pm2 logs $APP_NAME --lines 20
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# Show running containers
|
||||
print_status "Running containers:"
|
||||
docker compose ps
|
||||
|
||||
# Cleanup old images (keep last 3)
|
||||
print_status "Cleaning up old Docker images..."
|
||||
docker image prune -f >/dev/null 2>&1 || true
|
||||
# Show PM2 status
|
||||
print_status "PM2 Status:"
|
||||
pm2 status
|
||||
|
||||
print_success "🎉 Deployment completed successfully!"
|
||||
print_status "Application is now running at: http://localhost:3010"
|
||||
print_status "API health endpoint: http://localhost:3010/api/health"
|
||||
print_status "Application is now running at: http://localhost:$PORT"
|
||||
print_status "API health endpoint: http://localhost:$PORT/api/health"
|
||||
@@ -1,23 +0,0 @@
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3010:3000"
|
||||
environment:
|
||||
DATABASE_URL: ${DATABASE_URL}
|
||||
AZURE_OPENAI_KEY: ${AZURE_OPENAI_KEY}
|
||||
AZURE_OPENAI_ENDPOINT: ${AZURE_OPENAI_ENDPOINT}
|
||||
AZURE_OPENAI_DEPLOYMENT: ${AZURE_OPENAI_DEPLOYMENT}
|
||||
OLLAMA_API_URL: ${OLLAMA_API_URL}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
NEXTAUTH_URL: ${NEXTAUTH_URL}
|
||||
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
|
||||
NODE_ENV: production
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:3000/api/health || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
28
ecosystem.config.js
Normal file
28
ecosystem.config.js
Normal file
@@ -0,0 +1,28 @@
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: 'ghidul-biblic',
|
||||
script: 'npm',
|
||||
args: 'start',
|
||||
cwd: '/root/ghidul-biblic',
|
||||
instances: 1,
|
||||
autorestart: true,
|
||||
watch: false,
|
||||
max_memory_restart: '4G',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
PORT: 3010,
|
||||
HOSTNAME: '0.0.0.0',
|
||||
},
|
||||
env_production: {
|
||||
NODE_ENV: 'production',
|
||||
PORT: 3010,
|
||||
HOSTNAME: '0.0.0.0',
|
||||
},
|
||||
error_file: './logs/err.log',
|
||||
out_file: './logs/out.log',
|
||||
log_file: './logs/combined.log',
|
||||
time: true
|
||||
}
|
||||
]
|
||||
};
|
||||
13
i18n.ts
13
i18n.ts
@@ -1,14 +1,21 @@
|
||||
import {getRequestConfig} from 'next-intl/server';
|
||||
import ro from './messages/ro.json';
|
||||
import en from './messages/en.json';
|
||||
|
||||
// Can be imported from a shared config
|
||||
export const locales = ['ro', 'en'];
|
||||
export const locales = ['ro', 'en'] as const;
|
||||
|
||||
const messages = {
|
||||
ro,
|
||||
en
|
||||
} as const;
|
||||
|
||||
export default getRequestConfig(async ({locale}) => {
|
||||
// Ensure locale has a value, default to 'ro' if undefined
|
||||
const validLocale = locale || 'ro';
|
||||
const validLocale = (locale || 'ro') as keyof typeof messages;
|
||||
|
||||
return {
|
||||
locale: validLocale,
|
||||
messages: (await import(`./messages/${validLocale}.json`)).default
|
||||
messages: messages[validLocale]
|
||||
};
|
||||
});
|
||||
@@ -2,16 +2,13 @@ export function isTokenExpired(token: string): boolean {
|
||||
try {
|
||||
const payload = JSON.parse(atob(token.split('.')[1])) as { exp?: number }
|
||||
if (!payload || !payload.exp) {
|
||||
console.log('Token has no expiration data')
|
||||
return true
|
||||
return true
|
||||
}
|
||||
|
||||
const currentTime = Math.floor(Date.now() / 1000)
|
||||
const isExpired = payload.exp < currentTime
|
||||
console.log(`Token expiration check: exp=${payload.exp}, now=${currentTime}, expired=${isExpired}`)
|
||||
return isExpired
|
||||
} catch (error) {
|
||||
console.log('Token validation error:', error)
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -19,11 +16,8 @@ export function isTokenExpired(token: string): boolean {
|
||||
export function clearExpiredToken(): void {
|
||||
const token = localStorage.getItem('authToken')
|
||||
if (token && isTokenExpired(token)) {
|
||||
console.log('Clearing expired token from localStorage')
|
||||
localStorage.removeItem('authToken')
|
||||
} else if (token) {
|
||||
console.log('Token exists and is valid')
|
||||
} else {
|
||||
console.log('No token in localStorage')
|
||||
}
|
||||
}
|
||||
@@ -33,33 +33,33 @@
|
||||
},
|
||||
"home": {
|
||||
"hero": {
|
||||
"title": "Biblical Guide",
|
||||
"subtitle": "Explore Scripture with artificial intelligence",
|
||||
"description": "A modern platform for Bible study, with intelligent AI chat, advanced search, and a prayer community that supports you on your spiritual journey.",
|
||||
"title": "Biblical Guide – Online Bible Study with AI Chat, Daily Verses, and Prayer Community",
|
||||
"subtitle": "Online Bible Study with AI assistance",
|
||||
"description": "Biblical Guide is an online Bible study app. Read Scripture, ask questions with AI-powered chat, search verses instantly, and join a global prayer community that supports your spiritual growth.",
|
||||
"cta": {
|
||||
"readBible": "Start reading",
|
||||
"askAI": "Ask AI"
|
||||
"askAI": "Try it free now – AI Bible chat"
|
||||
},
|
||||
"liveCounter": "Join thousands of believers studying God's word right now"
|
||||
"liveCounter": "Join thousands of believers who use Biblical Guide to study, understand, and apply God's Word in their everyday lives"
|
||||
},
|
||||
"features": {
|
||||
"title": "Discover the features",
|
||||
"subtitle": "Everything you need for a complete Bible study experience",
|
||||
"bible": {
|
||||
"title": "Read the Bible",
|
||||
"description": "Explore Scripture with a modern and easy-to-use interface"
|
||||
"title": "Read the Bible online",
|
||||
"description": "access all 66 books with a modern and intuitive interface"
|
||||
},
|
||||
"chat": {
|
||||
"title": "AI Chat",
|
||||
"description": "Ask questions about Scripture and receive clear answers"
|
||||
"title": "AI Bible Chat",
|
||||
"description": "ask Scripture questions and get clear, accurate answers"
|
||||
},
|
||||
"prayers": {
|
||||
"title": "Prayers",
|
||||
"description": "Share prayers and pray together with the community"
|
||||
"title": "Prayer Community",
|
||||
"description": "share requests and join others in prayer"
|
||||
},
|
||||
"search": {
|
||||
"title": "Search",
|
||||
"description": "Search for verses and passages throughout Scripture"
|
||||
"title": "Verse Search",
|
||||
"description": "quickly find verses, keywords, and topics across the Bible"
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
@@ -80,7 +80,7 @@
|
||||
"tryButton": "Try it yourself"
|
||||
},
|
||||
"dailyVerse": {
|
||||
"title": "Today's Verse",
|
||||
"title": "Daily Bible Verse – receive encouragement from Scripture every day, straight to your inbox",
|
||||
"date": "January 15, 2024",
|
||||
"verse": "For I know the plans I have for you, declares the Lord, plans to prosper you and not to harm you, plans to give you hope and a future.",
|
||||
"reference": "Jeremiah 29:11",
|
||||
@@ -481,5 +481,15 @@
|
||||
"back": "Back",
|
||||
"next": "Next",
|
||||
"previous": "Previous"
|
||||
},
|
||||
"seo": {
|
||||
"title": "Biblical Guide – Online Bible Study with AI, Daily Verses & Prayer Community",
|
||||
"description": "Biblical Guide is an online Bible study app with AI-powered chat, instant verse search, and a global prayer community. Get daily Bible verses and Scripture-based answers to your questions.",
|
||||
"keywords": "online Bible study, AI Bible chat, daily Bible verse, Bible study app, prayer community, read the Bible online, verse search, Scripture study",
|
||||
"ogTitle": "Biblical Guide – Online Bible Study with AI",
|
||||
"ogDescription": "Read the Bible online, ask questions with AI chat, and join a prayer community. Get daily Bible verses in your inbox.",
|
||||
"twitterTitle": "Biblical Guide – Online Bible Study with AI",
|
||||
"twitterDescription": "Online Bible study app with AI chat, daily verses, and prayer community.",
|
||||
"footer": "Biblical Guide – online Bible study app with AI chat, daily verses, and prayer community."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,33 +33,33 @@
|
||||
},
|
||||
"home": {
|
||||
"hero": {
|
||||
"title": "Ghid Biblic",
|
||||
"subtitle": "Explorează Scriptura cu ajutorul inteligenței artificiale",
|
||||
"description": "O platformă modernă pentru studiul Bibliei, cu chat AI inteligent, căutare avansată și o comunitate de rugăciune care te sprijină în călătoria ta spirituală.",
|
||||
"title": "Ghid Biblic – Studiu biblic online cu AI, versete zilnice și comunitate de rugăciune",
|
||||
"subtitle": "Studiu biblic online cu asistență AI",
|
||||
"description": "Ghid Biblic este o aplicație de studiu biblic online. Citește Scriptura, pune întrebări cu ajutorul chatului AI, caută versete rapid și alătură-te unei comunități de rugăciune care te sprijină zilnic.",
|
||||
"cta": {
|
||||
"readBible": "Începe să citești",
|
||||
"askAI": "Întreabă AI"
|
||||
"askAI": "Încearcă acum gratuit - Chat AI"
|
||||
},
|
||||
"liveCounter": "Alătură-te la mii de credincioși care studiază Cuvântul lui Dumnezeu chiar acum"
|
||||
"liveCounter": "Alătură-te miilor de credincioși care folosesc Ghid Biblic pentru a înțelege și aplica Cuvântul lui Dumnezeu în viața de zi cu zi"
|
||||
},
|
||||
"features": {
|
||||
"title": "Descoperă funcționalitățile",
|
||||
"subtitle": "Totul de ce ai nevoie pentru o experiență completă de studiu biblic",
|
||||
"bible": {
|
||||
"title": "Citește Biblia",
|
||||
"description": "Explorează Scriptura cu o interfață modernă și ușor de folosit"
|
||||
"title": "Citește Biblia online",
|
||||
"description": "toate cele 66 de cărți biblice într-o interfață modernă și intuitivă"
|
||||
},
|
||||
"chat": {
|
||||
"title": "Chat cu AI",
|
||||
"description": "Pune întrebări despre Scriptură și primește răspunsuri clare"
|
||||
"title": "Chat AI biblic",
|
||||
"description": "întreabă Scriptura și primește răspunsuri clare, fundamentate pe versete"
|
||||
},
|
||||
"prayers": {
|
||||
"title": "Rugăciuni",
|
||||
"description": "Partajează rugăciuni și roagă-te împreună cu comunitatea"
|
||||
"title": "Comunitate de rugăciune",
|
||||
"description": "trimite și primește cereri de rugăciune"
|
||||
},
|
||||
"search": {
|
||||
"title": "Căutare",
|
||||
"description": "Caută versete și pasaje din întreaga Scriptură"
|
||||
"title": "Căutare versete",
|
||||
"description": "găsește rapid pasaje, cuvinte cheie și teme biblice"
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
@@ -80,7 +80,7 @@
|
||||
"tryButton": "Încearcă și tu"
|
||||
},
|
||||
"dailyVerse": {
|
||||
"title": "Versetul de Astăzi",
|
||||
"title": "Versetul biblic al zilei – primește zilnic inspirație din Scriptură direct în inbox",
|
||||
"date": "15 ianuarie 2024",
|
||||
"verse": "Căci Eu știu gândurile pe care le am cu privire la voi, zice Domnul, gânduri de pace și nu de rău, ca să vă dau un viitor și o speranță.",
|
||||
"reference": "Ieremia 29:11",
|
||||
@@ -481,5 +481,15 @@
|
||||
"back": "Înapoi",
|
||||
"next": "Următorul",
|
||||
"previous": "Anterior"
|
||||
},
|
||||
"seo": {
|
||||
"title": "Ghid Biblic – Studiu Biblic Online cu AI, Versete Zilnice și Comunitate de Rugăciune",
|
||||
"description": "Ghid Biblic este o aplicație de studiu biblic online cu chat AI, căutare rapidă de versete și o comunitate de rugăciune. Primește versete zilnice și răspunsuri clare din Scriptură.",
|
||||
"keywords": "studiu biblic online, aplicație biblică, chat AI biblic, versete biblice zilnice, comunitate de rugăciune, citește Biblia online, căutare versete, Biblia Română",
|
||||
"ogTitle": "Ghid Biblic – Studiu Biblic Online cu AI",
|
||||
"ogDescription": "Citește Biblia online, pune întrebări prin chat AI și alătură-te unei comunități de rugăciune. Primește versete zilnice în inbox.",
|
||||
"twitterTitle": "Ghid Biblic – Studiu Biblic Online cu AI",
|
||||
"twitterDescription": "Aplicație biblică online cu chat AI, versete zilnice și comunitate de rugăciune.",
|
||||
"footer": "Ghid Biblic – aplicație de studiu biblic online, cu chat AI, versete zilnice și comunitate de rugăciune."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import type { NextRequest } from 'next/server'
|
||||
// Avoid importing Node-only modules in Middleware (Edge runtime)
|
||||
import createIntlMiddleware from 'next-intl/middleware'
|
||||
import { locales } from './i18n'
|
||||
|
||||
// Internationalization configuration
|
||||
const intlMiddleware = createIntlMiddleware({
|
||||
locales: ['ro', 'en'],
|
||||
defaultLocale: 'ro'
|
||||
locales: [...locales],
|
||||
defaultLocale: 'ro',
|
||||
localePrefix: 'always'
|
||||
})
|
||||
|
||||
// Note: Avoid using Prisma or any Node-only APIs in Middleware.
|
||||
@@ -71,10 +72,11 @@ export const config = {
|
||||
// Match all pathnames except for
|
||||
// - api routes
|
||||
// - _next (Next.js internals)
|
||||
// - _vercel
|
||||
// - static files (images, etc.)
|
||||
'/((?!api|_next|_vercel|.*\\..*).*)',
|
||||
// However, match all pathnames within `/api`, except for the Middleware to run there
|
||||
'/api/:path*',
|
||||
'/dashboard/:path*'
|
||||
// - favicon.ico, robots.txt, sitemap.xml
|
||||
'/((?!api|_next|_vercel|.*\\..*|favicon.ico|robots.txt|sitemap.xml).*)',
|
||||
// Match internationalized pathnames
|
||||
'/(ro|en)/:path*'
|
||||
],
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@ const withNextIntl = require('next-intl/plugin')('./i18n.ts');
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'standalone',
|
||||
typedRoutes: false,
|
||||
trailingSlash: false,
|
||||
poweredByHeader: false,
|
||||
compress: true,
|
||||
}
|
||||
|
||||
module.exports = withNextIntl(nextConfig)
|
||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 663 B |
Reference in New Issue
Block a user