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 }) => + `
${escapeHtml(p.id)}
`, + ) + .join("\n"); + photosHtml = ` +
+
+${photoItems} +
+
`; + } + const content = ` -

${escapeHtml(w.name)}

-

${escapeHtml(w.description)}

- `; +
+
Карта (загрузится после инициализации 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",