Исправлена проблема пагинации (добавлена сортировка по id) Добавлена функция получения данных по статьям для sitemap для SEO
This commit is contained in:
parent
3a495766ce
commit
d3ef4f6f45
@ -7,7 +7,7 @@ import { AuthRequest } from '../../../middleware/auth';
|
|||||||
import { checkPermission } from '../../../utils/permissions';
|
import { checkPermission } from '../../../utils/permissions';
|
||||||
import { logger } from '../../../config/logger';
|
import { logger } from '../../../config/logger';
|
||||||
import { Article } from "../../../types";
|
import { Article } from "../../../types";
|
||||||
import { uploadBufferToS3 } from "../../../services/s3Service";
|
import { bucketName, uploadBufferToS3 } from "../../../services/s3Service";
|
||||||
|
|
||||||
const DEFAULT_COVER_IMAGE = '/images/cover-placeholder.webp';
|
const DEFAULT_COVER_IMAGE = '/images/cover-placeholder.webp';
|
||||||
|
|
||||||
@ -286,12 +286,12 @@ export async function importArticles(req: AuthRequest, res: Response) : Promise<
|
|||||||
|
|
||||||
if (!req.user) {
|
if (!req.user) {
|
||||||
res.status(401).json({ error: 'Пользователь не вошел в систему' });
|
res.status(401).json({ error: 'Пользователь не вошел в систему' });
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Array.isArray(articles) || articles.length === 0) {
|
if (!Array.isArray(articles) || articles.length === 0) {
|
||||||
res.status(400).json({ message: 'Ожидается непустой массив статей в теле запроса' });
|
res.status(400).json({ message: 'Ожидается непустой массив статей в теле запроса' });
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let importedCount = 0;
|
let importedCount = 0;
|
||||||
@ -302,6 +302,7 @@ export async function importArticles(req: AuthRequest, res: Response) : Promise<
|
|||||||
// Шаг 1: Создание статьи
|
// Шаг 1: Создание статьи
|
||||||
const newArticle = await prisma.article.create({
|
const newArticle = await prisma.article.create({
|
||||||
data: {
|
data: {
|
||||||
|
importId: article.importId,
|
||||||
title: article.title,
|
title: article.title,
|
||||||
excerpt: article.excerpt,
|
excerpt: article.excerpt,
|
||||||
content: article.content,
|
content: article.content,
|
||||||
@ -336,23 +337,89 @@ export async function importArticles(req: AuthRequest, res: Response) : Promise<
|
|||||||
(contentType.includes('image/') ? `.${contentType.split('/')[1]}` : '');
|
(contentType.includes('image/') ? `.${contentType.split('/')[1]}` : '');
|
||||||
|
|
||||||
// Генерация имени файла
|
// Генерация имени файла
|
||||||
const filename = `cover-${Date.now()}${fileExtension}`;
|
const fileName = `cover-${Date.now()}${fileExtension}`;
|
||||||
|
const webpFileName = path.basename(fileName, path.extname(fileName)) + '.webp';
|
||||||
|
|
||||||
// Загрузка буфера в S3
|
// Загрузка буфера в S3
|
||||||
const uploadedUrl = await uploadBufferToS3(
|
const uploadedUrl = await uploadBufferToS3(
|
||||||
imageBuffer,
|
imageBuffer,
|
||||||
folder,
|
folder,
|
||||||
filename,
|
webpFileName,
|
||||||
contentType,
|
contentType,
|
||||||
'original',
|
'medium',
|
||||||
80
|
80
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Добавить обложку к статье
|
||||||
|
await prisma.article.update({
|
||||||
|
where: { id: newArticle.id },
|
||||||
|
data: {
|
||||||
|
coverImage: uploadedUrl
|
||||||
|
},
|
||||||
|
});
|
||||||
} catch (imageError) {
|
} catch (imageError) {
|
||||||
console.error(`Ошибка загрузки coverImage для статьи ${article.id}:`, imageError);
|
console.error(`Ошибка загрузки coverImage для статьи ${article.id}:`, imageError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Шаг 3: Обработка массива images
|
||||||
|
let updatedContent = newArticle.content;
|
||||||
|
if (article.images && Array.isArray(article.images) && article.images.length > 0) {
|
||||||
|
const folder = `articles/${newArticle.id}/images`;
|
||||||
|
const uploadedImageUrls: string[] = [];
|
||||||
|
|
||||||
|
// Загружаем каждое изображение в S3
|
||||||
|
for (let i = 0; i < article.images.length; i++) {
|
||||||
|
const imageUrl = article.images[i];
|
||||||
|
try {
|
||||||
|
const imageResponse = await axios.get(imageUrl, {responseType: 'arraybuffer'});
|
||||||
|
const imageBuffer = Buffer.from(imageResponse.data, 'binary');
|
||||||
|
const contentType = imageResponse.headers['content-type'];
|
||||||
|
const fileExtension = path.extname(new URL(imageUrl).pathname) ||
|
||||||
|
(contentType.includes('image/') ? `.${contentType.split('/')[1]}` : '');
|
||||||
|
const fileName = `image-${Date.now()}-${i}${fileExtension}`;
|
||||||
|
const webpFileName = path.basename(fileName, path.extname(fileName)) + '.webp';
|
||||||
|
|
||||||
|
const uploadedUrl = await uploadBufferToS3(
|
||||||
|
imageBuffer,
|
||||||
|
folder,
|
||||||
|
webpFileName,
|
||||||
|
contentType,
|
||||||
|
'medium',
|
||||||
|
80
|
||||||
|
);
|
||||||
|
uploadedImageUrls.push(uploadedUrl);
|
||||||
|
} catch (imageError) {
|
||||||
|
console.error(`Ошибка загрузки изображения ${imageUrl} для статьи ${newArticle.id}:`, imageError);
|
||||||
|
uploadedImageUrls.push(''); // Добавляем пустую строку, чтобы сохранить порядок
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Заменяем плейсхолдеры {{image1}}, {{image2}} и т.д. на uploadedUrl
|
||||||
|
updatedContent = newArticle.content;
|
||||||
|
const hasPlaceholders = uploadedImageUrls.some((_, index) => updatedContent.includes(`{{image${index + 1}}}`));
|
||||||
|
if (!hasPlaceholders) {
|
||||||
|
console.warn(`В content статьи ${newArticle.id} отсутствуют плейсхолдеры для изображений`);
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadedImageUrls.forEach((uploadedUrl, index) => {
|
||||||
|
if (uploadedUrl) {
|
||||||
|
const placeholder = `{{image${index + 1}}}`;
|
||||||
|
const imgTag = `<img src="${uploadedUrl}" alt="" scale="0.9" style="transform: scale(0.9)">`;
|
||||||
|
const regex = new RegExp(placeholder, 'g');
|
||||||
|
updatedContent = updatedContent.replace(regex, imgTag);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обновляем поле content в базе данных
|
||||||
|
await prisma.article.update({
|
||||||
|
where: {id: newArticle.id},
|
||||||
|
data: {
|
||||||
|
content: updatedContent,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
importedCount++;
|
importedCount++;
|
||||||
} catch (articleError) {
|
} catch (articleError) {
|
||||||
console.error(`Ошибка импорта статьи ${article.id}:`, articleError);
|
console.error(`Ошибка импорта статьи ${article.id}:`, articleError);
|
||||||
@ -369,4 +436,3 @@ export async function importArticles(req: AuthRequest, res: Response) : Promise<
|
|||||||
await prisma.$disconnect();
|
await prisma.$disconnect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,7 +45,10 @@ export async function listArticles(req: Request, res: Response) {
|
|||||||
},
|
},
|
||||||
skip,
|
skip,
|
||||||
take: perPage,
|
take: perPage,
|
||||||
orderBy: { publishedAt: 'desc' },
|
orderBy: [
|
||||||
|
{ publishedAt: 'desc' },
|
||||||
|
{ id: 'desc' }, // вторичный критерий сортировки
|
||||||
|
]
|
||||||
}),
|
}),
|
||||||
prisma.article.count({ where }),
|
prisma.article.count({ where }),
|
||||||
]);
|
]);
|
||||||
|
26
src/routes/articles/controllers/sitemap.ts
Normal file
26
src/routes/articles/controllers/sitemap.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { prisma } from '../../../lib/prisma';
|
||||||
|
|
||||||
|
|
||||||
|
export async function sitemapArticles(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const articles = await prisma.article.findMany({
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
publishedAt: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
take: 10000, // запасной лимит
|
||||||
|
});
|
||||||
|
|
||||||
|
// Формируем ответ
|
||||||
|
res.json({
|
||||||
|
articles,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка получения статей для карты сайта:', error);
|
||||||
|
res.status(500).json({ error: 'Серверная ошибка' });
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { auth } from '../../middleware/auth';
|
import { auth } from '../../middleware/auth';
|
||||||
import { searchArticles } from './controllers/search';
|
import { searchArticles } from './controllers/search';
|
||||||
|
import { sitemapArticles } from './controllers/sitemap';
|
||||||
import { listArticles } from './controllers/list';
|
import { listArticles } from './controllers/list';
|
||||||
import {
|
import {
|
||||||
getArticle,
|
getArticle,
|
||||||
@ -16,6 +17,7 @@ const router = express.Router();
|
|||||||
|
|
||||||
// Поиск и список routes
|
// Поиск и список routes
|
||||||
router.get('/search', searchArticles);
|
router.get('/search', searchArticles);
|
||||||
|
router.get('/sitemap', sitemapArticles);
|
||||||
router.get('/', listArticles);
|
router.get('/', listArticles);
|
||||||
|
|
||||||
// Импорт route
|
// Импорт route
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
|
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import multer from 'multer';
|
import multer from 'multer';
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
@ -103,12 +103,12 @@ export const uploadToS3 = async (
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Функция для загрузки буфера в S3
|
* Функция для загрузки буфера в S3
|
||||||
* @param buffer - Буфер для загрузки
|
* @param buffer - Буфер для загрузки
|
||||||
* @param folder - Путь к папке в S3
|
* @param folder - Путь к папке в S3
|
||||||
* @param filename - Опциональное имя файла (если пусто, то будет сгенерирован UUID как имя)
|
* @param filename - Опциональное имя файла (если пусто, то будет сгенерирован UUID как имя)
|
||||||
* @param mimetype - MIME тип контента буфера
|
* @param mimetype - MIME тип контента буфера
|
||||||
* @param resolutionId - Опциональный идентификатор разрешения изображения
|
* @param resolutionId - Опциональный идентификатор разрешения изображения
|
||||||
* @param quality - Отцтональное значение качества при оптимизации
|
* @param quality - Опциональное значение качества при оптимизации
|
||||||
* @returns URL загрущенного файла
|
* @returns URL загрущенного файла
|
||||||
*/
|
*/
|
||||||
export const uploadBufferToS3 = async (
|
export const uploadBufferToS3 = async (
|
||||||
@ -149,18 +149,18 @@ export const uploadBufferToS3 = async (
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Возвращаем URL файла
|
// Возвращаем URL файла с заменой расширения
|
||||||
const fileUrl = `https://${bucketName}.s3.regru.cloud/${finalFilename}`;
|
const fileUrl = `https://${bucketName}.s3.regru.cloud/${key}`;
|
||||||
logger.info(`Buffer successfully uploaded to S3: ${key}`);
|
logger.info(`Буффер успешно загружен в S3: ${key}`);
|
||||||
return fileUrl;
|
return fileUrl;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error uploading buffer to S3');
|
logger.error('Ошибка заргузки буффера в S3');
|
||||||
console.error(error);
|
console.error(error);
|
||||||
throw new Error('Error uploading buffer to S3');
|
throw new Error('Ошибка заргузки буффера в S3');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Функция получения пасширения файла из MIME типа
|
// Функция получения расширения файла из MIME типа
|
||||||
function getExtensionFromMimeType(mimetype: string): string {
|
function getExtensionFromMimeType(mimetype: string): string {
|
||||||
const mimeToExt: Record<string, string> = {
|
const mimeToExt: Record<string, string> = {
|
||||||
'image/jpeg': '.jpg',
|
'image/jpeg': '.jpg',
|
||||||
|
@ -8,6 +8,7 @@ export interface Article {
|
|||||||
cityId: number;
|
cityId: number;
|
||||||
author: Author;
|
author: Author;
|
||||||
coverImage: string;
|
coverImage: string;
|
||||||
|
images?: string[];
|
||||||
gallery?: GalleryImage[];
|
gallery?: GalleryImage[];
|
||||||
publishedAt: string;
|
publishedAt: string;
|
||||||
readTime: number;
|
readTime: number;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user