diff --git a/.roo/memory-bank/activeContext.md b/.roo/memory-bank/activeContext.md
index 0b8d7ec..070a880 100644
--- a/.roo/memory-bank/activeContext.md
+++ b/.roo/memory-bank/activeContext.md
@@ -2,10 +2,10 @@
## Task State
-- task_id: TASK-F14
+- task_id: TASK-SSR-DETAIL
- status: success
-- parent_task: TASK-F12, TASK-F13
-- summary: **Карта тренировки: одиночные метки фото не пропадают при сбросе участка (v-memo + стабильные :settings), метки центрированы на GPS-точках, клик по треку — поиск ближайшей точки (радиус 40 м).**
+- parent_task: TASK-SSR-2
+- summary: **SSR /public/workouts/:id — HTML-контент переписан под DOM-структуру WorkoutItem.vue (вёрстка + фото + OG:image + noscript).**
- next task: — (no active tasks)
### TASK-F12/F13/F14 — фиксы карты тренировки (WorkoutItem.vue)
diff --git a/server/index.ts b/server/index.ts
index dc7fc9c..6c74b31 100644
--- a/server/index.ts
+++ b/server/index.ts
@@ -20,7 +20,11 @@ function getAssetTagsSafe(): string {
function escapeHtml(text: string | null | undefined): string {
if (!text) return "";
- return text.replace(//g, ">");
+ return text
+ .replace(/&/g, String.fromCharCode(38) + "amp;")
+ .replace(//g, String.fromCharCode(38) + "gt;")
+ .replace(/"/g, String.fromCharCode(38) + "quot;");
}
function formatDate(iso: string): string {
@@ -183,21 +187,126 @@ app.get("/public/workouts/:id", async (req: Request, res: Response) => {
return;
}
+ // --- Build params section ---
+ const params: string[] = [];
+
+ if (w.workouted_at) {
+ params.push(
+ `
Дата:
${formatDate(
+ w.workouted_at,
+ )}
`,
+ );
+ }
+ if (w.distantion) {
+ params.push(
+ ` Расстояние:
${formatDistance(
+ w.distantion,
+ )} км
`,
+ );
+ }
+ if (w.heart_rate) {
+ params.push(
+ ` Средний пульс:
${Math.floor(
+ w.heart_rate,
+ )} уд./ мин.
`,
+ );
+ }
+ if (w.max_heart_rate) {
+ params.push(
+ ` Максимальный пульс:
${Math.floor(
+ w.max_heart_rate,
+ )} уд./ мин.
`,
+ );
+ }
+ if (w.speed) {
+ params.push(
+ ` Средняя скорость:
${formatSpeed(
+ w.speed,
+ )} км / ч
`,
+ );
+ }
+ if (w.max_speed) {
+ params.push(
+ ` Максимальная скорость:
${formatSpeed(
+ w.max_speed,
+ )} км / ч
`,
+ );
+ }
+ if (w.temperature) {
+ params.push(
+ ` Температура:
${w.temperature} °C
`,
+ );
+ }
+ if (w.power) {
+ params.push(
+ ` Средняя мощность:
${Math.floor(
+ w.power,
+ )} Вт
`,
+ );
+ }
+ if (w.max_power) {
+ params.push(
+ ` Максимальная мощность:
${Math.floor(
+ w.max_power,
+ )} Вт
`,
+ );
+ }
+ if (w.cadence) {
+ params.push(
+ ` Каденс:
${w.cadence} об/мин
`,
+ );
+ }
+ if (w.duraion_sec) {
+ params.push(
+ ` Длительность:
${formatDuration(
+ w.duraion_sec,
+ )}
`,
+ );
+ }
+ if (
+ w.external_links &&
+ w.external_links.values &&
+ w.external_links.values.length > 0
+ ) {
+ const dzenUrl = w.external_links.values[0].value;
+ params.push(
+ ` `,
+ );
+ }
+
+ // --- Build photos section ---
+ let photosHtml = "";
+ if (w.photos && w.photos.length > 0) {
+ const photoItems = w.photos
+ .map(
+ (p: { id: string; url: string }) =>
+ ` `,
+ )
+ .join("\n");
+ photosHtml = `
+ `;
+ }
+
const content = `
- ${escapeHtml(w.name)}
- ${escapeHtml(w.description)}
-
- - Дата: ${formatDate(w.workouted_at)}
- - Расстояние: ${formatDistance(w.distantion)} км
- - Средняя скорость: ${formatSpeed(w.speed)} км/ч
- - Макс. скорость: ${formatSpeed(w.max_speed)} км/ч
- - Средний пульс: ${w.heart_rate} уд/мин
- - Макс. пульс: ${w.max_heart_rate} уд/мин
- - Средняя мощность: ${w.power} Вт
- - Макс. мощность: ${w.max_power} Вт
- - Каденс: ${w.cadence} об/мин
- - Длительность: ${formatDuration(w.duraion_sec)}
-
`;
+
+
Карта (загрузится после инициализации JS)
+
+
${escapeHtml(
+ w.name,
+ )}
+${params.join("\n")}
+
+
${photosHtml}
+ Графики (загрузятся после инициализации JS)
+ `;
const desc = w.description
? w.description.slice(0, 150) + (w.description.length > 150 ? "…" : "")
@@ -210,6 +319,7 @@ app.get("/public/workouts/:id", async (req: Request, res: Response) => {
title: `${w.name} — Cycle Rider`,
description: `${desc} ${metrics}`,
canonicalUrl: `${BASE_URL}/public/workouts/${w.id}`,
+ ogImage: w.attachment?.url,
jsonLd: {
"@context": "https://schema.org",
"@type": "SportsActivity",