Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ INTERNAL_URL=http://localhost:3000
COCKPIT_API_URL=https://your-cockpit-url.com/
COCKPIT_API_KEY=your-cockpit-api-key

# On-demand revalidation
REVALIDATE_SECRET=your-random-secret-min-32-chars

# Yandex Smart Captcha
NEXT_PUBLIC_CAPTCHA_SITE_KEY=your-yandex-captcha-site-key
CAPTCHA_SECRET=your-yandex-captcha-secret-key
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,13 @@ jobs:
COCKPIT_API_URL: ${{ secrets.COCKPIT_API_URL }}
COCKPIT_API_KEY: ${{ secrets.COCKPIT_API_KEY }}
YANDEX_MAPS_API_KEY: ${{ secrets.YANDEX_MAPS_API_KEY }}
REVALIDATE_SECRET: ${{ secrets.REVALIDATE_SECRET }}
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
port: ${{ secrets.VPS_PORT || 22 }}
envs: IMAGE_NAME,NEXT_PUBLIC_SITE_URL,NEXT_PUBLIC_CAPTCHA_SITE_KEY,CAPTCHA_SECRET,COCKPIT_API_URL,COCKPIT_API_KEY,YANDEX_MAPS_API_KEY
envs: IMAGE_NAME,NEXT_PUBLIC_SITE_URL,NEXT_PUBLIC_CAPTCHA_SITE_KEY,CAPTCHA_SECRET,COCKPIT_API_URL,COCKPIT_API_KEY,YANDEX_MAPS_API_KEY,REVALIDATE_SECRET
script: |
set -euo pipefail
echo "🚀 Starting deployment process..."
Expand All @@ -104,6 +105,7 @@ jobs:
"CAPTCHA_SECRET=${CAPTCHA_SECRET}" \
"COCKPIT_API_URL=${COCKPIT_API_URL}" \
"COCKPIT_API_KEY=${COCKPIT_API_KEY}" \
"REVALIDATE_SECRET=${REVALIDATE_SECRET}" \
> .env
echo "✅ .env file written"
echo "🔍 .env keys (values masked):"
Expand Down
78 changes: 78 additions & 0 deletions src/app/api/revalidate/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { NextRequest, NextResponse } from 'next/server'
import { revalidateTag } from 'next/cache'

/**
* POST /api/revalidate
* Эндпоинт для ревалидации кеша Next.js по тегам, связанным с моделями данных.
*
* Пример запроса:
* curl -X POST http://localhost:3000/api/revalidate \
* -H "Content-Type: application/json" \
* -H "x-webhook-secret: your_secret_here" \
* -d '{"model": "news"}'
*
* В ответ возвращает JSON с результатом ревалидации.
*/
export async function POST(request: NextRequest) {
try {
const secret = request.headers.get('x-webhook-secret')
const expectedSecret = process.env.REVALIDATE_SECRET

if (!expectedSecret) {
console.error('REVALIDATE_SECRET is not configured')
return NextResponse.json({ error: 'Server configuration error' }, { status: 500 })
}

if (secret !== expectedSecret) {
console.warn('Invalid webhook secret received')
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

const body = await request.json()
const { model } = body

if (!model) {
return NextResponse.json({ error: 'Model parameter is required' }, { status: 400 })
}

const modelTagMap: Record<string, string[]> = {
// Collections
news: ['collection-news'],
works: ['collection-works'],
reviews: ['collection-reviews'],
category: ['collection-category'],
faq: ['collection-faq'],
advantages: ['collection-advantages'],
createprocess: ['collection-createprocess'],
restoration: ['singleton-restoration'],
masters: ['collection-masters'],
mainslider: ['collection-mainslider'],
// Singletons
maininfo: ['singleton-maininfo'],
order: ['singleton-order'],
// Trees
gallery: ['tree-gallery'],
}

const tagsToRevalidate = modelTagMap[model] || []

if (tagsToRevalidate.length === 0) {
return NextResponse.json({ error: `Unknown model: ${model}` }, { status: 400 })
}

for (const tag of tagsToRevalidate) {
revalidateTag(tag)
console.log(`Revalidated tag: ${tag}`)
}

return NextResponse.json({
success: true,
revalidated: true,
tags: tagsToRevalidate,
timestamp: new Date().toISOString(),
})
} catch (err) {
console.error('Revalidation error:', err)
return NextResponse.json({ error: 'Failed to revalidate' }, { status: 500 })
}
}
2 changes: 0 additions & 2 deletions src/app/categories/[categories-detail]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import Heading from '@/components/Heading/Heading'
import Detail from '@/components/Detail/Detail'
import { fetchCollectionItem, getImageUrl } from '@/lib/api-client'

export const dynamic = 'force-dynamic'

type PageProps = {
params: Promise<{
'categories-detail': string
Expand Down
2 changes: 0 additions & 2 deletions src/app/gallery/[...slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ import {
buildGalleryBreadcrumbs,
} from '@/functions/gallery'

export const dynamic = 'force-dynamic'

type PageParams = {
params: Promise<{
slug: string[]
Expand Down
2 changes: 0 additions & 2 deletions src/app/in-stock/[in-stock-detail]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import Detail from '@/components/Detail/Detail'
import Master from '@/components/Master/Master'
import { fetchCollectionItem, getImageUrl } from '@/lib/api-client'

export const dynamic = 'force-dynamic'

type PageProps = {
params: Promise<{
'in-stock-detail': string
Expand Down
2 changes: 2 additions & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import Header from '@/components/Header/Header'
import Footer from '@/components/Footer/Footer'
import ScrollButton from '@/components/ScrollButton/ScrollButton'

export const dynamic = 'force-dynamic'

type LayoutProps = {
children?: React.ReactNode
}
Expand Down
2 changes: 0 additions & 2 deletions src/app/news/[news-detail]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import Heading from '@/components/Heading/Heading'
import Detail from '@/components/Detail/Detail'
import { fetchCollectionItem, getImageUrl } from '@/lib/api-client'

export const dynamic = 'force-dynamic'

type PageParams = {
params: Promise<{
'news-detail': string
Expand Down
2 changes: 0 additions & 2 deletions src/app/works/[works-detail]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import Detail from '@/components/Detail/Detail'
import Master from '@/components/Master/Master'
import { fetchCollectionItem, getImageUrl } from '@/lib/api-client'

export const dynamic = 'force-dynamic'

type PageProps = {
params: Promise<{
'works-detail': string
Expand Down
8 changes: 4 additions & 4 deletions src/lib/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export async function fetchSingleton<T = unknown>(

try {
const response = await fetch(url, {
cache: options.cache ?? 'no-store',
cache: options.cache ?? 'force-cache',
next: {
...(options.revalidate !== undefined && { revalidate: options.revalidate }),
tags: options.tags || [`singleton-${model}`],
Expand Down Expand Up @@ -106,7 +106,7 @@ export async function fetchCollection<T = unknown>(

try {
const response = await fetch(url, {
cache: options.cache ?? 'no-store',
cache: options.cache ?? 'force-cache',
next: {
...(options.revalidate !== undefined && { revalidate: options.revalidate }),
tags: options.tags || [`collection-${model}`],
Expand Down Expand Up @@ -144,7 +144,7 @@ export async function fetchCollectionItem<T = unknown>(

try {
const response = await fetch(url, {
cache: options.cache ?? 'no-store',
cache: options.cache ?? 'force-cache',
next: {
...(options.revalidate !== undefined && { revalidate: options.revalidate }),
tags: options.tags || [`collection-${model}-${id}`],
Expand Down Expand Up @@ -182,7 +182,7 @@ export async function fetchTree<T = unknown>(

try {
const response = await fetch(url, {
cache: options.cache ?? 'no-store',
cache: options.cache ?? 'force-cache',
next: {
...(options.revalidate !== undefined && { revalidate: options.revalidate }),
tags: options.tags || [`tree-${model}`],
Expand Down