@webnumseoagent/next 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.mjs +17 -9
- package/blog.tsx +41 -14
- package/dist/blog.d.ts +10 -0
- package/dist/blog.js +34 -23
- package/package.json +1 -1
package/bin/cli.mjs
CHANGED
|
@@ -370,13 +370,14 @@ async function init() {
|
|
|
370
370
|
log(`\n И смонтируй JSON-LD в layout (внутри <body>):\n \x1b[36mimport { SeoJsonLd } from "${importJsonld}";\n <SeoJsonLd route="/" />\x1b[0m`);
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
-
// 7. Блог (пакет-рендер, Фаза 3.1c) —
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
|
|
373
|
+
// 7. Блог (пакет-рендер, Фаза 3.1c) — ПО УМОЛЧАНИЮ: ставит роуты /blog и /blog/[slug] прямо при
|
|
374
|
+
// подключении сайта (opt-out флагом --no-blog). ensureFile идемпотентен и НЕ перезаписывает файлы,
|
|
375
|
+
// поэтому собственный блог сайта (свой app/blog/page.*) остаётся нетронутым. ПОСЛЕ авто-обёртки
|
|
376
|
+
// generateMetadata: у блог-роута свой generateMetadata (seoBlogPost), его НЕ оборачиваем seoMeta.
|
|
377
|
+
if (!args.includes("--no-blog")) {
|
|
377
378
|
const jsxExt = ext === "ts" ? "tsx" : "jsx";
|
|
378
379
|
const mdParam = ext === "ts" ? "{ params }: { params: { slug: string } }" : "{ params }";
|
|
379
|
-
await ensureFile(
|
|
380
|
+
const madeIndex = await ensureFile(
|
|
380
381
|
path.join(root, appDir, `blog/page.${jsxExt}`),
|
|
381
382
|
`import { SeoBlogIndex } from "${blogFor("blog/page.tsx")}";\nexport const revalidate = 300;\nexport default function BlogPage() {\n return <SeoBlogIndex title="Блог" />;\n}\n`,
|
|
382
383
|
);
|
|
@@ -388,16 +389,21 @@ async function init() {
|
|
|
388
389
|
const names = await fs.readdir(blogDir).catch(() => []);
|
|
389
390
|
blogDynConflict = names.some((n) => /^\[.*\]$/.test(n) && n !== "[slug]");
|
|
390
391
|
}
|
|
392
|
+
let madeArticle = false;
|
|
391
393
|
if (blogDynConflict) {
|
|
392
|
-
log(`\n \x1b[1mУ сайта уже есть app
|
|
394
|
+
log(`\n \x1b[1mУ сайта уже есть app/blog/[иной-сегмент]\x1b[0m — роут статьи /blog/[slug] не создаю (Next запрещает два динамических сегмента).`);
|
|
393
395
|
log(` Подключи статьи к своему сегменту вручную: \x1b[36mimport { seoBlogPost, SeoBlogArticle } from "${blogFor("blog/[slug]/page.tsx")}";\x1b[0m`);
|
|
394
396
|
} else {
|
|
395
|
-
await ensureFile(
|
|
397
|
+
madeArticle = await ensureFile(
|
|
396
398
|
path.join(root, appDir, `blog/[slug]/page.${jsxExt}`),
|
|
397
399
|
`import { notFound } from "next/navigation";\nimport { seoBlogPost, SeoBlogArticle } from "${blogFor("blog/[slug]/page.tsx")}";\nexport const revalidate = 300;\n\nexport async function generateMetadata(${mdParam}) {\n const post = await seoBlogPost(params.slug);\n if (!post) return {};\n return { title: post.title, description: post.excerpt, openGraph: { images: post.cover ? [post.cover] : [] } };\n}\n\nexport default async function BlogPostPage(${mdParam}) {\n const post = await seoBlogPost(params.slug);\n if (!post) notFound();\n return <SeoBlogArticle post={post} />;\n}\n`,
|
|
398
400
|
);
|
|
399
401
|
}
|
|
400
|
-
|
|
402
|
+
if (madeIndex || madeArticle) {
|
|
403
|
+
log(`\n \x1b[32m✓\x1b[0m Блог: роуты /blog (пакет-рендер) созданы. Настрой доставку + anon-ключ на платформе. (Отключить блог: \x1b[1m--no-blog\x1b[0m)`);
|
|
404
|
+
} else {
|
|
405
|
+
log(`\n \x1b[33mℹ\x1b[0m Блог: у сайта уже есть роуты /blog — не трогаю.`);
|
|
406
|
+
}
|
|
401
407
|
}
|
|
402
408
|
|
|
403
409
|
log("\n\x1b[1mЗатем:\x1b[0m задай эти же env-переменные в Vercel (Settings → Environment Variables) и сделай git push.\n");
|
|
@@ -466,5 +472,7 @@ async function findMetadataPages(dir, acc = []) {
|
|
|
466
472
|
if (cmd === "init") {
|
|
467
473
|
init().catch((e) => { console.error(e); process.exit(1); });
|
|
468
474
|
} else {
|
|
469
|
-
log("Использование: npx @webnumseoagent/next init [--site <id>] [--token <t>] [--api <url>] [--revalidate-secret <s>]");
|
|
475
|
+
log("Использование: npx @webnumseoagent/next init [--wrap] [--no-blog] [--site <id>] [--token <t>] [--api <url>] [--revalidate-secret <s>]");
|
|
476
|
+
log(" --wrap — авто-обернуть generateMetadata страниц (иначе печатает сниппеты)");
|
|
477
|
+
log(" --no-blog — не создавать роуты блога /blog (по умолчанию блог создаётся)");
|
|
470
478
|
}
|
package/blog.tsx
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
// app/blog/[slug]/page.tsx: const post = await seoBlogPost(params.slug); if(!post) notFound();
|
|
15
15
|
// return <SeoBlogArticle post={post} />
|
|
16
16
|
|
|
17
|
+
import type { CSSProperties } from "react";
|
|
18
|
+
|
|
17
19
|
const API = process.env.SEOAGENT_API_BASE ?? "";
|
|
18
20
|
const SITE = process.env.SEOAGENT_SITE_ID ?? "";
|
|
19
21
|
const TOKEN = process.env.SEOAGENT_TOKEN ?? "";
|
|
@@ -23,12 +25,16 @@ export interface SeoBlogMapping {
|
|
|
23
25
|
title: string; excerpt?: string; body: string;
|
|
24
26
|
id?: string; image?: string; published?: string; publishedAt?: string;
|
|
25
27
|
}
|
|
28
|
+
// Тема блога от платформы (config-API): цвета/скругления как ВАЛИДНЫЕ CSS-значения, которые пакет
|
|
29
|
+
// кладёт в свои переменные --sa-*. Сейчас платформа шлёт только accent (бренд-цвет владельца);
|
|
30
|
+
// border/surface/muted/radius зарезервированы под будущий ИИ-профиль дизайна сайта.
|
|
31
|
+
export interface SeoBlogTheme { accent?: string; border?: string; muted?: string; surface?: string; thBg?: string; radius?: string }
|
|
26
32
|
export interface SeoBlogConfig {
|
|
27
33
|
supabaseUrl: string; anonKey: string; table: string;
|
|
28
|
-
format: "html" | "minimarkup"; locales: string[]; mapping: SeoBlogMapping;
|
|
34
|
+
format: "html" | "minimarkup"; locales: string[]; mapping: SeoBlogMapping; theme?: SeoBlogTheme;
|
|
29
35
|
}
|
|
30
36
|
export interface SeoBlogSummary { slug: string; title: string; excerpt: string; cover: string | null; date: string | null }
|
|
31
|
-
export interface SeoBlogPostFull extends SeoBlogSummary { bodyHtml: string }
|
|
37
|
+
export interface SeoBlogPostFull extends SeoBlogSummary { bodyHtml: string; theme?: SeoBlogTheme }
|
|
32
38
|
|
|
33
39
|
function firstLocale(cfg: SeoBlogConfig): string {
|
|
34
40
|
return (cfg.locales ?? []).filter(Boolean)[0] ?? "";
|
|
@@ -113,6 +119,7 @@ async function fetchPost(cfg: SeoBlogConfig, slug: string, locale: string): Prom
|
|
|
113
119
|
cover: m.image ? (r[m.image] as string | null) ?? null : null,
|
|
114
120
|
date: m.publishedAt ? (r[m.publishedAt] as string | null) ?? null : null,
|
|
115
121
|
bodyHtml: cfg.format === "minimarkup" ? miniMarkupToHtml(rawBody) : sanitizeDeliveryHtml(rawBody),
|
|
122
|
+
theme: cfg.theme, // тему тащим с постом — чтобы SeoBlogArticle применил её без отдельного запроса
|
|
116
123
|
};
|
|
117
124
|
}
|
|
118
125
|
|
|
@@ -209,17 +216,37 @@ function sanitizeDeliveryHtml(html: string): string {
|
|
|
209
216
|
}
|
|
210
217
|
|
|
211
218
|
// ── Компоненты (серверные) ──────────────────────────────────────────────────────────────────
|
|
212
|
-
|
|
219
|
+
// Цвета/рамки/скругления — через СОБСТВЕННЫЕ переменные пакета (--sa-*) с нейтральным дефолтом.
|
|
220
|
+
// НЕ ссылаемся на токены сайта напрямую: shadcn/Tailwind хранят их как сырые HSL-компоненты
|
|
221
|
+
// (--primary: 222 47% 11%), в color/border это невалидно и ломает вид. Реальные цвета сайта
|
|
222
|
+
// проставит платформа: config-API отдаст профиль, а хост-обёртка выставит --sa-* валидными цветами.
|
|
223
|
+
// Шрифт и базовый цвет текста наследуются от layout сайта (color:inherit, без font-family).
|
|
224
|
+
const BODY_CSS = `.seoagent-blog-body{color:inherit}
|
|
225
|
+
.seoagent-blog-body img{max-width:100%;height:auto;border-radius:var(--sa-radius,8px);margin:1rem 0}
|
|
213
226
|
.seoagent-blog-body table{width:100%;border-collapse:collapse;margin:1rem 0;font-size:.95em}
|
|
214
|
-
.seoagent-blog-body th,.seoagent-blog-body td{border:1px solid
|
|
215
|
-
.seoagent-blog-body th{background
|
|
227
|
+
.seoagent-blog-body th,.seoagent-blog-body td{border:1px solid var(--sa-border,#e5e7eb);padding:.5rem .75rem;text-align:left}
|
|
228
|
+
.seoagent-blog-body th{background:var(--sa-th-bg,#f8fafc)}
|
|
216
229
|
.seoagent-blog-body h2{margin:1.75rem 0 .75rem;font-size:1.5rem;line-height:1.3}
|
|
217
230
|
.seoagent-blog-body h3{margin:1.4rem 0 .6rem;font-size:1.2rem}
|
|
218
231
|
.seoagent-blog-body p{margin:0 0 1rem}
|
|
219
|
-
.seoagent-blog-body a{color
|
|
232
|
+
.seoagent-blog-body a{color:var(--sa-accent,#2563eb)}
|
|
220
233
|
.seoagent-blog-body figure{margin:1.25rem 0}
|
|
221
|
-
.seoagent-blog-body figcaption{color
|
|
222
|
-
.seoagent-blog-body blockquote{margin:1rem 0;padding:.5rem 1rem;border-left:3px solid
|
|
234
|
+
.seoagent-blog-body figcaption{color:var(--sa-muted,#94a3b8);font-size:.85em;text-align:center;margin-top:.4rem}
|
|
235
|
+
.seoagent-blog-body blockquote{margin:1rem 0;padding:.5rem 1rem;border-left:3px solid var(--sa-border,#e5e7eb);color:var(--sa-muted,#475569)}`;
|
|
236
|
+
|
|
237
|
+
// Тема → CSS-переменные пакета (--sa-*). Кладём на корень блока — они каскадируются вниз к
|
|
238
|
+
// ссылкам/карточкам/телу. Отдаём только заданные ключи (пустые не трогаем → останется fallback).
|
|
239
|
+
function themeStyle(theme?: SeoBlogTheme): Record<string, string> {
|
|
240
|
+
const t = theme ?? {};
|
|
241
|
+
const out: Record<string, string> = {};
|
|
242
|
+
if (t.accent) out["--sa-accent"] = t.accent;
|
|
243
|
+
if (t.border) out["--sa-border"] = t.border;
|
|
244
|
+
if (t.muted) out["--sa-muted"] = t.muted;
|
|
245
|
+
if (t.surface) out["--sa-surface"] = t.surface;
|
|
246
|
+
if (t.thBg) out["--sa-th-bg"] = t.thBg;
|
|
247
|
+
if (t.radius) out["--sa-radius"] = t.radius;
|
|
248
|
+
return out;
|
|
249
|
+
}
|
|
223
250
|
|
|
224
251
|
// Список статей блога. Сам тянет конфиг+статьи. Пусто/ошибка → null (ничего не ломаем).
|
|
225
252
|
export async function SeoBlogIndex(props: { locale?: string; basePath?: string; limit?: number; title?: string }) {
|
|
@@ -230,16 +257,16 @@ export async function SeoBlogIndex(props: { locale?: string; basePath?: string;
|
|
|
230
257
|
if (!posts.length) return null;
|
|
231
258
|
const basePath = (props.basePath ?? "/blog").replace(/\/+$/, "");
|
|
232
259
|
return (
|
|
233
|
-
<div style={{ maxWidth: 1080, margin: "0 auto", padding: "1.5rem" }}>
|
|
260
|
+
<div style={{ maxWidth: 1080, margin: "0 auto", padding: "1.5rem", ...themeStyle(cfg.theme) } as CSSProperties}>
|
|
234
261
|
{props.title ? <h1 style={{ fontSize: "2rem", margin: "0 0 1.5rem" }}>{props.title}</h1> : null}
|
|
235
262
|
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(280px, 1fr))", gap: "1.5rem" }}>
|
|
236
263
|
{posts.map((p) => (
|
|
237
264
|
<a key={p.slug} href={`${basePath}/${p.slug}`}
|
|
238
|
-
style={{ display: "block", textDecoration: "none", color: "inherit", border: "1px solid #e5e7eb", borderRadius:
|
|
265
|
+
style={{ display: "block", textDecoration: "none", color: "inherit", border: "1px solid var(--sa-border, #e5e7eb)", borderRadius: "var(--sa-radius, 12px)", overflow: "hidden", background: "var(--sa-surface, transparent)" }}>
|
|
239
266
|
{p.cover ? <img src={p.cover} alt={p.title} style={{ width: "100%", height: 180, objectFit: "cover", display: "block" }} /> : null}
|
|
240
267
|
<div style={{ padding: "1rem" }}>
|
|
241
268
|
<h2 style={{ fontSize: "1.1rem", margin: "0 0 .5rem", lineHeight: 1.3 }}>{p.title}</h2>
|
|
242
|
-
{p.excerpt ? <p style={{ margin: 0, color: "#64748b", fontSize: ".9rem", lineHeight: 1.5 }}>{p.excerpt}</p> : null}
|
|
269
|
+
{p.excerpt ? <p style={{ margin: 0, color: "var(--sa-muted, #64748b)", fontSize: ".9rem", lineHeight: 1.5 }}>{p.excerpt}</p> : null}
|
|
243
270
|
</div>
|
|
244
271
|
</a>
|
|
245
272
|
))}
|
|
@@ -251,11 +278,11 @@ export async function SeoBlogIndex(props: { locale?: string; basePath?: string;
|
|
|
251
278
|
// Одна статья. Принимает пост (из seoBlogPost) — так страница может сделать notFound()/метаданные.
|
|
252
279
|
export function SeoBlogArticle({ post, locale }: { post: SeoBlogPostFull; locale?: string }) {
|
|
253
280
|
return (
|
|
254
|
-
<article style={{ maxWidth: 760, margin: "0 auto", padding: "1.5rem", lineHeight: 1.7 }}>
|
|
281
|
+
<article style={{ maxWidth: 760, margin: "0 auto", padding: "1.5rem", lineHeight: 1.7, ...themeStyle(post.theme) } as CSSProperties}>
|
|
255
282
|
<style dangerouslySetInnerHTML={{ __html: BODY_CSS }} />
|
|
256
|
-
{post.cover ? <img src={post.cover} alt={post.title} style={{ width: "100%", borderRadius:
|
|
283
|
+
{post.cover ? <img src={post.cover} alt={post.title} style={{ width: "100%", borderRadius: "var(--sa-radius, 12px)", marginBottom: "1.5rem" }} /> : null}
|
|
257
284
|
<h1 style={{ fontSize: "2.2rem", lineHeight: 1.2, margin: "0 0 1rem" }}>{post.title}</h1>
|
|
258
|
-
{post.date ? <p style={{ color: "#94a3b8", margin: "0 0 1.5rem" }}>{fmtDate(post.date, locale)}</p> : null}
|
|
285
|
+
{post.date ? <p style={{ color: "var(--sa-muted, #94a3b8)", margin: "0 0 1.5rem" }}>{fmtDate(post.date, locale)}</p> : null}
|
|
259
286
|
{/* Тело уже санитайзено при доставке (Фаза 3.1b) — рендерим как HTML. */}
|
|
260
287
|
<div className="seoagent-blog-body" dangerouslySetInnerHTML={{ __html: post.bodyHtml }} />
|
|
261
288
|
</article>
|
package/dist/blog.d.ts
CHANGED
|
@@ -7,6 +7,14 @@ export interface SeoBlogMapping {
|
|
|
7
7
|
published?: string;
|
|
8
8
|
publishedAt?: string;
|
|
9
9
|
}
|
|
10
|
+
export interface SeoBlogTheme {
|
|
11
|
+
accent?: string;
|
|
12
|
+
border?: string;
|
|
13
|
+
muted?: string;
|
|
14
|
+
surface?: string;
|
|
15
|
+
thBg?: string;
|
|
16
|
+
radius?: string;
|
|
17
|
+
}
|
|
10
18
|
export interface SeoBlogConfig {
|
|
11
19
|
supabaseUrl: string;
|
|
12
20
|
anonKey: string;
|
|
@@ -14,6 +22,7 @@ export interface SeoBlogConfig {
|
|
|
14
22
|
format: "html" | "minimarkup";
|
|
15
23
|
locales: string[];
|
|
16
24
|
mapping: SeoBlogMapping;
|
|
25
|
+
theme?: SeoBlogTheme;
|
|
17
26
|
}
|
|
18
27
|
export interface SeoBlogSummary {
|
|
19
28
|
slug: string;
|
|
@@ -24,6 +33,7 @@ export interface SeoBlogSummary {
|
|
|
24
33
|
}
|
|
25
34
|
export interface SeoBlogPostFull extends SeoBlogSummary {
|
|
26
35
|
bodyHtml: string;
|
|
36
|
+
theme?: SeoBlogTheme;
|
|
27
37
|
}
|
|
28
38
|
export declare function seoBlogList(opts?: {
|
|
29
39
|
locale?: string;
|
package/dist/blog.js
CHANGED
|
@@ -5,21 +5,6 @@ exports.seoBlogPost = seoBlogPost;
|
|
|
5
5
|
exports.SeoBlogIndex = SeoBlogIndex;
|
|
6
6
|
exports.SeoBlogArticle = SeoBlogArticle;
|
|
7
7
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
8
|
-
// @webnumseoagent/next/blog — рендер блога код-сайта (Фаза 3.1c).
|
|
9
|
-
// Пакет тянет ПУБЛИЧНЫЙ конфиг блога с платформы (config-API /blog: supabaseUrl + anonKey +
|
|
10
|
-
// таблица/формат/локали/маппинг), затем читает ОПУБЛИКОВАННЫЕ статьи прямо из Supabase клиента
|
|
11
|
-
// (anon-ключ + public-read RLS) и рендерит обложку + заголовок + тело (картинки/таблицы/ссылки).
|
|
12
|
-
//
|
|
13
|
-
// Тело статьи уже САНИТАЙЗЕНО на стороне платформы при доставке (Фаза 3.1b), поэтому здесь оно
|
|
14
|
-
// рендерится как HTML (dangerouslySetInnerHTML) — граница доверия проходит по доставке, не по рендеру.
|
|
15
|
-
//
|
|
16
|
-
// Все функции FAIL-SAFE: при ошибке/таймауте возвращают пусто — блог просто не покажет контент,
|
|
17
|
-
// сборка/рендер сайта НИКОГДА не падает.
|
|
18
|
-
//
|
|
19
|
-
// Использование (роуты ставит `npx @webnumseoagent/next init --blog`):
|
|
20
|
-
// app/blog/page.tsx: export default () => <SeoBlogIndex title="Блог" />
|
|
21
|
-
// app/blog/[slug]/page.tsx: const post = await seoBlogPost(params.slug); if(!post) notFound();
|
|
22
|
-
// return <SeoBlogArticle post={post} />
|
|
23
8
|
const API = process.env.SEOAGENT_API_BASE ?? "";
|
|
24
9
|
const SITE = process.env.SEOAGENT_SITE_ID ?? "";
|
|
25
10
|
const TOKEN = process.env.SEOAGENT_TOKEN ?? "";
|
|
@@ -114,6 +99,7 @@ async function fetchPost(cfg, slug, locale) {
|
|
|
114
99
|
cover: m.image ? r[m.image] ?? null : null,
|
|
115
100
|
date: m.publishedAt ? r[m.publishedAt] ?? null : null,
|
|
116
101
|
bodyHtml: cfg.format === "minimarkup" ? miniMarkupToHtml(rawBody) : sanitizeDeliveryHtml(rawBody),
|
|
102
|
+
theme: cfg.theme, // тему тащим с постом — чтобы SeoBlogArticle применил её без отдельного запроса
|
|
117
103
|
};
|
|
118
104
|
}
|
|
119
105
|
// ── Публичные data-функции (для generateMetadata / generateStaticParams) ──
|
|
@@ -229,17 +215,42 @@ function sanitizeDeliveryHtml(html) {
|
|
|
229
215
|
return s.trim();
|
|
230
216
|
}
|
|
231
217
|
// ── Компоненты (серверные) ──────────────────────────────────────────────────────────────────
|
|
232
|
-
|
|
218
|
+
// Цвета/рамки/скругления — через СОБСТВЕННЫЕ переменные пакета (--sa-*) с нейтральным дефолтом.
|
|
219
|
+
// НЕ ссылаемся на токены сайта напрямую: shadcn/Tailwind хранят их как сырые HSL-компоненты
|
|
220
|
+
// (--primary: 222 47% 11%), в color/border это невалидно и ломает вид. Реальные цвета сайта
|
|
221
|
+
// проставит платформа: config-API отдаст профиль, а хост-обёртка выставит --sa-* валидными цветами.
|
|
222
|
+
// Шрифт и базовый цвет текста наследуются от layout сайта (color:inherit, без font-family).
|
|
223
|
+
const BODY_CSS = `.seoagent-blog-body{color:inherit}
|
|
224
|
+
.seoagent-blog-body img{max-width:100%;height:auto;border-radius:var(--sa-radius,8px);margin:1rem 0}
|
|
233
225
|
.seoagent-blog-body table{width:100%;border-collapse:collapse;margin:1rem 0;font-size:.95em}
|
|
234
|
-
.seoagent-blog-body th,.seoagent-blog-body td{border:1px solid
|
|
235
|
-
.seoagent-blog-body th{background
|
|
226
|
+
.seoagent-blog-body th,.seoagent-blog-body td{border:1px solid var(--sa-border,#e5e7eb);padding:.5rem .75rem;text-align:left}
|
|
227
|
+
.seoagent-blog-body th{background:var(--sa-th-bg,#f8fafc)}
|
|
236
228
|
.seoagent-blog-body h2{margin:1.75rem 0 .75rem;font-size:1.5rem;line-height:1.3}
|
|
237
229
|
.seoagent-blog-body h3{margin:1.4rem 0 .6rem;font-size:1.2rem}
|
|
238
230
|
.seoagent-blog-body p{margin:0 0 1rem}
|
|
239
|
-
.seoagent-blog-body a{color
|
|
231
|
+
.seoagent-blog-body a{color:var(--sa-accent,#2563eb)}
|
|
240
232
|
.seoagent-blog-body figure{margin:1.25rem 0}
|
|
241
|
-
.seoagent-blog-body figcaption{color
|
|
242
|
-
.seoagent-blog-body blockquote{margin:1rem 0;padding:.5rem 1rem;border-left:3px solid
|
|
233
|
+
.seoagent-blog-body figcaption{color:var(--sa-muted,#94a3b8);font-size:.85em;text-align:center;margin-top:.4rem}
|
|
234
|
+
.seoagent-blog-body blockquote{margin:1rem 0;padding:.5rem 1rem;border-left:3px solid var(--sa-border,#e5e7eb);color:var(--sa-muted,#475569)}`;
|
|
235
|
+
// Тема → CSS-переменные пакета (--sa-*). Кладём на корень блока — они каскадируются вниз к
|
|
236
|
+
// ссылкам/карточкам/телу. Отдаём только заданные ключи (пустые не трогаем → останется fallback).
|
|
237
|
+
function themeStyle(theme) {
|
|
238
|
+
const t = theme ?? {};
|
|
239
|
+
const out = {};
|
|
240
|
+
if (t.accent)
|
|
241
|
+
out["--sa-accent"] = t.accent;
|
|
242
|
+
if (t.border)
|
|
243
|
+
out["--sa-border"] = t.border;
|
|
244
|
+
if (t.muted)
|
|
245
|
+
out["--sa-muted"] = t.muted;
|
|
246
|
+
if (t.surface)
|
|
247
|
+
out["--sa-surface"] = t.surface;
|
|
248
|
+
if (t.thBg)
|
|
249
|
+
out["--sa-th-bg"] = t.thBg;
|
|
250
|
+
if (t.radius)
|
|
251
|
+
out["--sa-radius"] = t.radius;
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
243
254
|
// Список статей блога. Сам тянет конфиг+статьи. Пусто/ошибка → null (ничего не ломаем).
|
|
244
255
|
async function SeoBlogIndex(props) {
|
|
245
256
|
const cfg = await fetchBlogConfig();
|
|
@@ -250,11 +261,11 @@ async function SeoBlogIndex(props) {
|
|
|
250
261
|
if (!posts.length)
|
|
251
262
|
return null;
|
|
252
263
|
const basePath = (props.basePath ?? "/blog").replace(/\/+$/, "");
|
|
253
|
-
return ((0, jsx_runtime_1.jsxs)("div", { style: { maxWidth: 1080, margin: "0 auto", padding: "1.5rem" }, children: [props.title ? (0, jsx_runtime_1.jsx)("h1", { style: { fontSize: "2rem", margin: "0 0 1.5rem" }, children: props.title }) : null, (0, jsx_runtime_1.jsx)("div", { style: { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(280px, 1fr))", gap: "1.5rem" }, children: posts.map((p) => ((0, jsx_runtime_1.jsxs)("a", { href: `${basePath}/${p.slug}`, style: { display: "block", textDecoration: "none", color: "inherit", border: "1px solid #e5e7eb", borderRadius:
|
|
264
|
+
return ((0, jsx_runtime_1.jsxs)("div", { style: { maxWidth: 1080, margin: "0 auto", padding: "1.5rem", ...themeStyle(cfg.theme) }, children: [props.title ? (0, jsx_runtime_1.jsx)("h1", { style: { fontSize: "2rem", margin: "0 0 1.5rem" }, children: props.title }) : null, (0, jsx_runtime_1.jsx)("div", { style: { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(280px, 1fr))", gap: "1.5rem" }, children: posts.map((p) => ((0, jsx_runtime_1.jsxs)("a", { href: `${basePath}/${p.slug}`, style: { display: "block", textDecoration: "none", color: "inherit", border: "1px solid var(--sa-border, #e5e7eb)", borderRadius: "var(--sa-radius, 12px)", overflow: "hidden", background: "var(--sa-surface, transparent)" }, children: [p.cover ? (0, jsx_runtime_1.jsx)("img", { src: p.cover, alt: p.title, style: { width: "100%", height: 180, objectFit: "cover", display: "block" } }) : null, (0, jsx_runtime_1.jsxs)("div", { style: { padding: "1rem" }, children: [(0, jsx_runtime_1.jsx)("h2", { style: { fontSize: "1.1rem", margin: "0 0 .5rem", lineHeight: 1.3 }, children: p.title }), p.excerpt ? (0, jsx_runtime_1.jsx)("p", { style: { margin: 0, color: "var(--sa-muted, #64748b)", fontSize: ".9rem", lineHeight: 1.5 }, children: p.excerpt }) : null] })] }, p.slug))) })] }));
|
|
254
265
|
}
|
|
255
266
|
// Одна статья. Принимает пост (из seoBlogPost) — так страница может сделать notFound()/метаданные.
|
|
256
267
|
function SeoBlogArticle({ post, locale }) {
|
|
257
|
-
return ((0, jsx_runtime_1.jsxs)("article", { style: { maxWidth: 760, margin: "0 auto", padding: "1.5rem", lineHeight: 1.7 }, children: [(0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: BODY_CSS } }), post.cover ? (0, jsx_runtime_1.jsx)("img", { src: post.cover, alt: post.title, style: { width: "100%", borderRadius:
|
|
268
|
+
return ((0, jsx_runtime_1.jsxs)("article", { style: { maxWidth: 760, margin: "0 auto", padding: "1.5rem", lineHeight: 1.7, ...themeStyle(post.theme) }, children: [(0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: BODY_CSS } }), post.cover ? (0, jsx_runtime_1.jsx)("img", { src: post.cover, alt: post.title, style: { width: "100%", borderRadius: "var(--sa-radius, 12px)", marginBottom: "1.5rem" } }) : null, (0, jsx_runtime_1.jsx)("h1", { style: { fontSize: "2.2rem", lineHeight: 1.2, margin: "0 0 1rem" }, children: post.title }), post.date ? (0, jsx_runtime_1.jsx)("p", { style: { color: "var(--sa-muted, #94a3b8)", margin: "0 0 1.5rem" }, children: fmtDate(post.date, locale) }) : null, (0, jsx_runtime_1.jsx)("div", { className: "seoagent-blog-body", dangerouslySetInnerHTML: { __html: post.bodyHtml } })] }));
|
|
258
269
|
}
|
|
259
270
|
function fmtDate(d, locale) {
|
|
260
271
|
try {
|
package/package.json
CHANGED