diff --git a/Dockerfile b/Dockerfile
index 725215d..6470778 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,17 +1,29 @@
# Stage 1: Build
FROM node:22-alpine AS builder
WORKDIR /app
-COPY package*.json ./
-RUN npm ci
+
+# Install pnpm via corepack
+RUN corepack enable && corepack prepare pnpm@latest --activate
+
+# Copy manifests first for layer caching
+COPY package.json pnpm-lock.yaml ./
+
+# Install all dependencies (including devDeps needed for build)
+RUN pnpm install --frozen-lockfile
+
+# Copy source and build
COPY . .
-RUN npm run build
+RUN pnpm build
# Stage 2: Runtime
FROM node:22-alpine AS runner
+WORKDIR /app
ENV NODE_ENV=production
ENV HOST=0.0.0.0
ENV PORT=3000
-WORKDIR /app
+
+# Nuxt SSR bundles all server deps into .output/server/
COPY --from=builder /app/.output /app/.output
+
EXPOSE 3000
CMD ["node", "/app/.output/server/index.mjs"]
diff --git a/app/data/site.ts b/app/data/site.ts
index e08c30e..0ffe12b 100644
--- a/app/data/site.ts
+++ b/app/data/site.ts
@@ -58,13 +58,13 @@ export const siteConfig: SiteConfig = {
},
{
id: 'telegram-bot',
- url: '#',
+ url: 'https://www.fiverr.com/users/mr_kayjaydee',
image: '/images/fiverr/telegram_bot.webp',
price: '$20',
},
{
id: 'website-development',
- url: '#',
+ url: 'https://www.fiverr.com/users/mr_kayjaydee',
image: '/images/fiverr/website.webp',
price: '$50',
},
@@ -96,7 +96,7 @@ export const siteConfig: SiteConfig = {
priceRange: '$$$',
aggregateRating: {
ratingValue: '5',
- reviewCount: '50',
+ reviewCount: '10',
},
},
},
diff --git a/package.json b/package.json
index 40cc97f..aab7b55 100644
--- a/package.json
+++ b/package.json
@@ -21,8 +21,8 @@
"nodemailer": "^8.0.5",
"nuxt": "^4.0.0",
"nuxt-gtag": "^4.1.0",
- "vue": "latest",
- "vue-router": "latest",
+ "vue": "^3.5.0",
+ "vue-router": "^4.5.0",
"zod": "^4.3.6"
},
"devDependencies": {
diff --git a/public/sitemap.xml b/public/sitemap.xml
deleted file mode 100644
index 89a5864..0000000
--- a/public/sitemap.xml
+++ /dev/null
@@ -1,86 +0,0 @@
-
-
-
-
-
- https://killiandalcin.fr/
- 2025-07-07
-
-
-
-
- https://killiandalcin.fr/fiverr
- 2025-07-07
-
-
-
-
- https://killiandalcin.fr/projects
- 2025-07-07
-
-
-
-
- https://killiandalcin.fr/contact
- 2025-07-07
-
-
-
-
- https://killiandalcin.fr/about
- 2025-07-07
-
-
-
-
- https://killiandalcin.fr/formation
- 2025-07-07
-
-
-
-
-
-
- https://killiandalcin.fr/project/virtual-tour
- 2025-07-07
-
-
-
-
- https://killiandalcin.fr/project/xinko
- 2025-07-07
-
-
-
-
- https://killiandalcin.fr/project/image-manipulation
- 2025-07-07
-
-
-
-
- https://killiandalcin.fr/project/flowboard
- 2025-07-07
-
-
-
-
- https://killiandalcin.fr/project/primate-web-admin
- 2025-07-07
-
-
-
-
- https://killiandalcin.fr/project/instagram-bot
- 2025-07-07
-
-
-
-
- https://killiandalcin.fr/project/crowdin-status-bot
- 2025-07-07
-
-
-
\ No newline at end of file
diff --git a/server/plugins/rate-limit.ts b/server/plugins/rate-limit.ts
new file mode 100644
index 0000000..7ee6819
--- /dev/null
+++ b/server/plugins/rate-limit.ts
@@ -0,0 +1,32 @@
+const ipMap = new Map()
+
+// Clean stale entries every 5 minutes to prevent memory leak
+setInterval(() => {
+ const now = Date.now()
+ for (const [ip, entry] of ipMap) {
+ if (entry.reset < now) ipMap.delete(ip)
+ }
+}, 5 * 60 * 1000)
+
+export default defineNitroPlugin((nitro) => {
+ nitro.hooks.hook('request', (event) => {
+ // Only rate-limit the contact POST endpoint
+ if (event.method !== 'POST' || !event.path.startsWith('/api/contact')) return
+
+ const ip = getRequestIP(event, { xForwardedFor: true }) ?? 'unknown'
+ const now = Date.now()
+ const window = 60_000 // 1 minute window
+ const limit = 3 // max 3 requests per minute per IP
+
+ const entry = ipMap.get(ip)
+ if (!entry || entry.reset < now) {
+ ipMap.set(ip, { count: 1, reset: now + window })
+ return
+ }
+
+ entry.count++
+ if (entry.count > limit) {
+ throw createError({ statusCode: 429, message: 'Too many requests. Please try again later.' })
+ }
+ })
+})