Исправлена проблема пагинации (добавлена сортировка по 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 { logger } from '../../../config/logger';
|
||||
import { Article } from "../../../types";
|
||||
import { uploadBufferToS3 } from "../../../services/s3Service";
|
||||
import { bucketName, uploadBufferToS3 } from "../../../services/s3Service";
|
||||
|
||||
const DEFAULT_COVER_IMAGE = '/images/cover-placeholder.webp';
|
||||
|
||||
@ -286,12 +286,12 @@ export async function importArticles(req: AuthRequest, res: Response) : Promise<
|
||||
|
||||
if (!req.user) {
|
||||
res.status(401).json({ error: 'Пользователь не вошел в систему' });
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(articles) || articles.length === 0) {
|
||||
res.status(400).json({ message: 'Ожидается непустой массив статей в теле запроса' });
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
let importedCount = 0;
|
||||
@ -302,6 +302,7 @@ export async function importArticles(req: AuthRequest, res: Response) : Promise<
|
||||
// Шаг 1: Создание статьи
|
||||
const newArticle = await prisma.article.create({
|
||||
data: {
|
||||
importId: article.importId,
|
||||
title: article.title,
|
||||
excerpt: article.excerpt,
|
||||
content: article.content,
|
||||
@ -336,23 +337,89 @@ export async function importArticles(req: AuthRequest, res: Response) : Promise<
|
||||
(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
|
||||
const uploadedUrl = await uploadBufferToS3(
|
||||
imageBuffer,
|
||||
folder,
|
||||
filename,
|
||||
webpFileName,
|
||||
contentType,
|
||||
'original',
|
||||
'medium',
|
||||
80
|
||||
);
|
||||
|
||||
// Добавить обложку к статье
|
||||
await prisma.article.update({
|
||||
where: { id: newArticle.id },
|
||||
data: {
|
||||
coverImage: uploadedUrl
|
||||
},
|
||||
});
|
||||
} catch (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++;
|
||||
} catch (articleError) {
|
||||
console.error(`Ошибка импорта статьи ${article.id}:`, articleError);
|
||||
@ -369,4 +436,3 @@ export async function importArticles(req: AuthRequest, res: Response) : Promise<
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -45,7 +45,10 @@ export async function listArticles(req: Request, res: Response) {
|
||||
},
|
||||
skip,
|
||||
take: perPage,
|
||||
orderBy: { publishedAt: 'desc' },
|
||||
orderBy: [
|
||||
{ publishedAt: 'desc' },
|
||||
{ id: 'desc' }, // вторичный критерий сортировки
|
||||
]
|
||||
}),
|
||||
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 { auth } from '../../middleware/auth';
|
||||
import { searchArticles } from './controllers/search';
|
||||
import { sitemapArticles } from './controllers/sitemap';
|
||||
import { listArticles } from './controllers/list';
|
||||
import {
|
||||
getArticle,
|
||||
@ -16,6 +17,7 @@ const router = express.Router();
|
||||
|
||||
// Поиск и список routes
|
||||
router.get('/search', searchArticles);
|
||||
router.get('/sitemap', sitemapArticles);
|
||||
router.get('/', listArticles);
|
||||
|
||||
// Импорт 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 multer from 'multer';
|
||||
import sharp from 'sharp';
|
||||
@ -103,12 +103,12 @@ export const uploadToS3 = async (
|
||||
|
||||
/**
|
||||
* Функция для загрузки буфера в S3
|
||||
* @param buffer - Буфер для загрузки
|
||||
* @param folder - Путь к папке в S3
|
||||
* @param filename - Опциональное имя файла (если пусто, то будет сгенерирован UUID как имя)
|
||||
* @param mimetype - MIME тип контента буфера
|
||||
* @param buffer - Буфер для загрузки
|
||||
* @param folder - Путь к папке в S3
|
||||
* @param filename - Опциональное имя файла (если пусто, то будет сгенерирован UUID как имя)
|
||||
* @param mimetype - MIME тип контента буфера
|
||||
* @param resolutionId - Опциональный идентификатор разрешения изображения
|
||||
* @param quality - Отцтональное значение качества при оптимизации
|
||||
* @param quality - Опциональное значение качества при оптимизации
|
||||
* @returns URL загрущенного файла
|
||||
*/
|
||||
export const uploadBufferToS3 = async (
|
||||
@ -149,18 +149,18 @@ export const uploadBufferToS3 = async (
|
||||
})
|
||||
);
|
||||
|
||||
// Возвращаем URL файла
|
||||
const fileUrl = `https://${bucketName}.s3.regru.cloud/${finalFilename}`;
|
||||
logger.info(`Buffer successfully uploaded to S3: ${key}`);
|
||||
// Возвращаем URL файла с заменой расширения
|
||||
const fileUrl = `https://${bucketName}.s3.regru.cloud/${key}`;
|
||||
logger.info(`Буффер успешно загружен в S3: ${key}`);
|
||||
return fileUrl;
|
||||
} catch (error) {
|
||||
logger.error('Error uploading buffer to S3');
|
||||
logger.error('Ошибка заргузки буффера в S3');
|
||||
console.error(error);
|
||||
throw new Error('Error uploading buffer to S3');
|
||||
throw new Error('Ошибка заргузки буффера в S3');
|
||||
}
|
||||
};
|
||||
|
||||
// Функция получения пасширения файла из MIME типа
|
||||
// Функция получения расширения файла из MIME типа
|
||||
function getExtensionFromMimeType(mimetype: string): string {
|
||||
const mimeToExt: Record<string, string> = {
|
||||
'image/jpeg': '.jpg',
|
||||
|
@ -8,6 +8,7 @@ export interface Article {
|
||||
cityId: number;
|
||||
author: Author;
|
||||
coverImage: string;
|
||||
images?: string[];
|
||||
gallery?: GalleryImage[];
|
||||
publishedAt: string;
|
||||
readTime: number;
|
||||
|
Loading…
x
Reference in New Issue
Block a user