@webnumseoagent/next 0.9.0 → 0.9.2
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 +26 -7
- package/blog-fs.ts +2 -1
- package/blog.tsx +11 -8
- package/dist/blog-fs.d.ts +1 -0
- package/dist/blog-fs.js +2 -0
- package/dist/blog.d.ts +1 -0
- package/dist/blog.js +11 -8
- package/package.json +1 -1
package/bin/cli.mjs
CHANGED
|
@@ -50,7 +50,7 @@ async function writeOurRoute(p, content) {
|
|
|
50
50
|
ok(path.relative(root, p));
|
|
51
51
|
return;
|
|
52
52
|
}
|
|
53
|
-
if (!/SeoBlogIndex|SeoBlogArticle|seoBlogPost/.test(cur)) { skip(`${path.relative(root, p)} — свой блог сайта`); return; }
|
|
53
|
+
if (!/SeoBlogIndex|SeoBlogArticle|seoBlogPost|seoBlogUrlPrefix/.test(cur)) { skip(`${path.relative(root, p)} — свой блог сайта`); return; }
|
|
54
54
|
if (cur === content) { skip(path.relative(root, p)); return; }
|
|
55
55
|
await fs.writeFile(p, content);
|
|
56
56
|
ok(`${path.relative(root, p)} (обновлён)`);
|
|
@@ -755,13 +755,32 @@ async function blogPage() {
|
|
|
755
755
|
if (localeDir) ok(`Мультиязычный сайт (${localeDir}) — блог под layout сайта: ${appDir}/${localeDir}/blog (header/footer сайта).`);
|
|
756
756
|
else ok(`Блог на ${appDir}/blog (наследует корневой layout сайта).`);
|
|
757
757
|
|
|
758
|
-
//
|
|
759
|
-
//
|
|
758
|
+
// Мультиязычный сайт: реальный блог лежит под /[locale]/blog, а голый /blog[/slug] иначе даёт 404
|
|
759
|
+
// (спам-бэклинки и сторонние ссылки часто без префикса локали). Ставим на корень app/blog РЕДИРЕКТ-СТАБЫ
|
|
760
|
+
// → 308 на каноническую /{defaultLocale}/blog[/slug] (префикс из .blogfs.json — та же каноника, что в
|
|
761
|
+
// sitemap). Статик "blog" бьёт динамический [locale], поэтому /blog не матчит [locale]="blog".
|
|
762
|
+
// writeOurRoute перезапишет НАШ прежний голый /blog (маркер SeoBlogIndex), но не тронет свой блог сайта.
|
|
760
763
|
if (localeDir) {
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
764
|
+
const redirImport = (file) => `import { notFound, permanentRedirect } from "next/navigation";\nimport { seoBlogUrlPrefix } from "${blogFor(file)}";`;
|
|
765
|
+
await writeOurRoute(
|
|
766
|
+
path.join(root, appDir, `blog/page.${jsxExt}`),
|
|
767
|
+
`${redirImport("blog/page.tsx")}\nexport const dynamic = "force-dynamic";\n\n// SEO Agent: локаль-less /blog → 308 на /{defaultLocale}/blog (иначе 404 на мультиязычном сайте).\nexport default async function BlogIndexRedirect() {\n const prefix = await seoBlogUrlPrefix();\n if (!prefix) notFound();\n permanentRedirect(\`\${prefix}/blog\`);\n}\n`,
|
|
768
|
+
);
|
|
769
|
+
// Конфликт: свой корневой blog/[иной-сегмент] → редирект-[slug] не ставим (Next запрещает два динам. имени).
|
|
770
|
+
let rootDynConflict = false;
|
|
771
|
+
const rootBlogDir = path.join(root, appDir, "blog");
|
|
772
|
+
if (await exists(rootBlogDir)) {
|
|
773
|
+
const names = await fs.readdir(rootBlogDir).catch(() => []);
|
|
774
|
+
rootDynConflict = names.some((n) => /^\[.*\]$/.test(n) && n !== "[slug]");
|
|
775
|
+
}
|
|
776
|
+
if (rootDynConflict) {
|
|
777
|
+
log(`\n \x1b[33mℹ\x1b[0m У сайта есть корневой blog/[иной-сегмент] — редирект /blog/[slug] не ставлю (конфликт динам. сегментов).`);
|
|
778
|
+
} else {
|
|
779
|
+
const rSig = ext === "ts" ? `{ params }: { params: Promise<{ slug: string }> }` : "{ params }";
|
|
780
|
+
await writeOurRoute(
|
|
781
|
+
path.join(root, appDir, `blog/[slug]/page.${jsxExt}`),
|
|
782
|
+
`${redirImport("blog/[slug]/page.tsx")}\nexport const dynamic = "force-dynamic";\n\n// SEO Agent: локаль-less /blog/[slug] → 308 на /{defaultLocale}/blog/[slug] (спасает бэклинки без префикса локали).\nexport default async function BlogSlugRedirect(${rSig}) {\n const { slug } = await params;\n const prefix = await seoBlogUrlPrefix();\n if (!prefix) notFound();\n permanentRedirect(\`\${prefix}/blog/\${slug}\`);\n}\n`,
|
|
783
|
+
);
|
|
765
784
|
}
|
|
766
785
|
}
|
|
767
786
|
|
package/blog-fs.ts
CHANGED
|
@@ -45,7 +45,8 @@ export function fsTheme(): SeoBlogTheme | undefined {
|
|
|
45
45
|
|
|
46
46
|
// Канонический префикс публичных URL блога. Приоритет — явный descriptor.urlPrefix (пишет платформа);
|
|
47
47
|
// фолбэк для старых дескрипторов без него: <2 локалей → "" (одноязычный /blog), иначе "/<defaultLocale>".
|
|
48
|
-
|
|
48
|
+
// Экспортируется для локаль-less редирект-роута (голый /blog → каноническая /{defaultLocale}/blog).
|
|
49
|
+
export function urlPrefix(): string {
|
|
49
50
|
const d = readDescriptor();
|
|
50
51
|
if (!d) return "";
|
|
51
52
|
if (typeof d.urlPrefix === "string") return d.urlPrefix.replace(/\/+$/, "");
|
package/blog.tsx
CHANGED
|
@@ -38,6 +38,13 @@ export async function seoBlogPost(slug: string, opts?: { locale?: string }): Pro
|
|
|
38
38
|
return m.fsBlogPost(slug, opts?.locale ?? m.fsDefaultLocale());
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
// Канонический префикс локали публичных URL блога ("" одноязычный | "/en" и т.п.). Для локаль-less
|
|
42
|
+
// редирект-роута мультиязычного сайта: голый /blog[/slug] → 308 на /{defaultLocale}/blog[/slug].
|
|
43
|
+
export async function seoBlogUrlPrefix(): Promise<string> {
|
|
44
|
+
const m = await blogfs();
|
|
45
|
+
return m.urlPrefix();
|
|
46
|
+
}
|
|
47
|
+
|
|
41
48
|
// Параметры для generateStaticParams роута блога: пре-рендер ВСЕХ статей на сборке (SSG из файлов).
|
|
42
49
|
// Без localeParam → [{slug}] (одноязычный /blog/[slug]). С localeParam → [{<localeParam>, slug}] для КАЖДОЙ
|
|
43
50
|
// локали (мультиязычный /[locale]/blog/[slug]): иначе force-static не покроет комбинацию locale×slug → 404.
|
|
@@ -124,18 +131,14 @@ function safeTocCss(css?: string): string {
|
|
|
124
131
|
return css.slice(0, 4000);
|
|
125
132
|
}
|
|
126
133
|
|
|
127
|
-
const TOC_CSS = `.sa-article-wrap{max-width:
|
|
134
|
+
const TOC_CSS = `.sa-article-wrap{max-width:1080px;margin:0 auto;padding:var(--sa-blog-top,clamp(5rem,8vw,7rem)) 1.5rem 3rem;container-type:inline-size}
|
|
128
135
|
.sa-article-grid{display:block}
|
|
129
136
|
.sa-article-grid:not(.has-toc){max-width:760px;margin:0 auto}
|
|
130
137
|
.sa-article{min-width:0;line-height:1.7}
|
|
131
|
-
.sa-toc{font-size:.9rem}
|
|
132
|
-
/*
|
|
138
|
+
.sa-toc{font-size:.9rem;margin-bottom:2.5rem}
|
|
139
|
+
/* Оглавление слева + статья рядом, БЛОК по центру — единый вид для любой ширины контента (без гуттер-режима). */
|
|
133
140
|
@container (min-width:700px){.sa-article-grid.has-toc{display:grid;column-gap:2.5rem;grid-template-columns:250px minmax(0,760px);justify-content:center;align-items:start}
|
|
134
|
-
.sa-toc{position:sticky;top:var(--sa-toc-top,88px)}}
|
|
135
|
-
/* Широкий контент (ZeroMax): статья ПО ЦЕНТРУ вьюпорта, оглавление в левом гуттере, прижато к статье (капнуто ~300px). */
|
|
136
|
-
@container (min-width:1180px){.sa-article-grid.has-toc{grid-template-columns:minmax(0,1fr) minmax(0,760px) minmax(0,1fr);column-gap:0}
|
|
137
|
-
.sa-article-grid.has-toc>.sa-toc{grid-column:1;justify-self:end;width:min(100%,300px);margin-right:2.5rem}
|
|
138
|
-
.sa-article-grid.has-toc>.sa-article{grid-column:2}}
|
|
141
|
+
.sa-toc{position:sticky;top:var(--sa-toc-top,88px);margin-bottom:0}}
|
|
139
142
|
.sa-toc>summary{list-style:none;font-weight:700;font-size:.95rem;margin:0 0 .75rem;display:flex;align-items:center;justify-content:space-between;gap:.5rem;color:inherit;cursor:pointer}
|
|
140
143
|
.sa-toc>summary::-webkit-details-marker{display:none}
|
|
141
144
|
.sa-toc>summary .sa-toc-chev{transition:transform .2s ease;color:var(--sa-muted,#94a3b8)}
|
package/dist/blog-fs.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SeoBlogSummary, SeoBlogPostFull, SeoBlogTheme } from "./blog";
|
|
2
2
|
export declare function fsTheme(): SeoBlogTheme | undefined;
|
|
3
|
+
export declare function urlPrefix(): string;
|
|
3
4
|
export declare function fsAvailable(): boolean;
|
|
4
5
|
export declare function fsDefaultLocale(): string;
|
|
5
6
|
export declare function fsLocales(): string[];
|
package/dist/blog-fs.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.fsTheme = fsTheme;
|
|
4
|
+
exports.urlPrefix = urlPrefix;
|
|
4
5
|
exports.fsAvailable = fsAvailable;
|
|
5
6
|
exports.fsDefaultLocale = fsDefaultLocale;
|
|
6
7
|
exports.fsLocales = fsLocales;
|
|
@@ -52,6 +53,7 @@ function fsTheme() {
|
|
|
52
53
|
}
|
|
53
54
|
// Канонический префикс публичных URL блога. Приоритет — явный descriptor.urlPrefix (пишет платформа);
|
|
54
55
|
// фолбэк для старых дескрипторов без него: <2 локалей → "" (одноязычный /blog), иначе "/<defaultLocale>".
|
|
56
|
+
// Экспортируется для локаль-less редирект-роута (голый /blog → каноническая /{defaultLocale}/blog).
|
|
55
57
|
function urlPrefix() {
|
|
56
58
|
const d = readDescriptor();
|
|
57
59
|
if (!d)
|
package/dist/blog.d.ts
CHANGED
|
@@ -33,6 +33,7 @@ export declare function seoBlogList(opts?: {
|
|
|
33
33
|
export declare function seoBlogPost(slug: string, opts?: {
|
|
34
34
|
locale?: string;
|
|
35
35
|
}): Promise<SeoBlogPostFull | null>;
|
|
36
|
+
export declare function seoBlogUrlPrefix(): Promise<string>;
|
|
36
37
|
export declare function seoBlogStaticParams(opts?: {
|
|
37
38
|
localeParam?: string;
|
|
38
39
|
}): Promise<Array<Record<string, string>>>;
|
package/dist/blog.js
CHANGED
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.seoBlogList = seoBlogList;
|
|
37
37
|
exports.seoBlogPost = seoBlogPost;
|
|
38
|
+
exports.seoBlogUrlPrefix = seoBlogUrlPrefix;
|
|
38
39
|
exports.seoBlogStaticParams = seoBlogStaticParams;
|
|
39
40
|
exports.SeoBlogIndex = SeoBlogIndex;
|
|
40
41
|
exports.SeoBlogArticle = SeoBlogArticle;
|
|
@@ -52,6 +53,12 @@ async function seoBlogPost(slug, opts) {
|
|
|
52
53
|
const m = await blogfs();
|
|
53
54
|
return m.fsBlogPost(slug, opts?.locale ?? m.fsDefaultLocale());
|
|
54
55
|
}
|
|
56
|
+
// Канонический префикс локали публичных URL блога ("" одноязычный | "/en" и т.п.). Для локаль-less
|
|
57
|
+
// редирект-роута мультиязычного сайта: голый /blog[/slug] → 308 на /{defaultLocale}/blog[/slug].
|
|
58
|
+
async function seoBlogUrlPrefix() {
|
|
59
|
+
const m = await blogfs();
|
|
60
|
+
return m.urlPrefix();
|
|
61
|
+
}
|
|
55
62
|
// Параметры для generateStaticParams роута блога: пре-рендер ВСЕХ статей на сборке (SSG из файлов).
|
|
56
63
|
// Без localeParam → [{slug}] (одноязычный /blog/[slug]). С localeParam → [{<localeParam>, slug}] для КАЖДОЙ
|
|
57
64
|
// локали (мультиязычный /[locale]/blog/[slug]): иначе force-static не покроет комбинацию locale×slug → 404.
|
|
@@ -142,18 +149,14 @@ function safeTocCss(css) {
|
|
|
142
149
|
return "";
|
|
143
150
|
return css.slice(0, 4000);
|
|
144
151
|
}
|
|
145
|
-
const TOC_CSS = `.sa-article-wrap{max-width:
|
|
152
|
+
const TOC_CSS = `.sa-article-wrap{max-width:1080px;margin:0 auto;padding:var(--sa-blog-top,clamp(5rem,8vw,7rem)) 1.5rem 3rem;container-type:inline-size}
|
|
146
153
|
.sa-article-grid{display:block}
|
|
147
154
|
.sa-article-grid:not(.has-toc){max-width:760px;margin:0 auto}
|
|
148
155
|
.sa-article{min-width:0;line-height:1.7}
|
|
149
|
-
.sa-toc{font-size:.9rem}
|
|
150
|
-
/*
|
|
156
|
+
.sa-toc{font-size:.9rem;margin-bottom:2.5rem}
|
|
157
|
+
/* Оглавление слева + статья рядом, БЛОК по центру — единый вид для любой ширины контента (без гуттер-режима). */
|
|
151
158
|
@container (min-width:700px){.sa-article-grid.has-toc{display:grid;column-gap:2.5rem;grid-template-columns:250px minmax(0,760px);justify-content:center;align-items:start}
|
|
152
|
-
.sa-toc{position:sticky;top:var(--sa-toc-top,88px)}}
|
|
153
|
-
/* Широкий контент (ZeroMax): статья ПО ЦЕНТРУ вьюпорта, оглавление в левом гуттере, прижато к статье (капнуто ~300px). */
|
|
154
|
-
@container (min-width:1180px){.sa-article-grid.has-toc{grid-template-columns:minmax(0,1fr) minmax(0,760px) minmax(0,1fr);column-gap:0}
|
|
155
|
-
.sa-article-grid.has-toc>.sa-toc{grid-column:1;justify-self:end;width:min(100%,300px);margin-right:2.5rem}
|
|
156
|
-
.sa-article-grid.has-toc>.sa-article{grid-column:2}}
|
|
159
|
+
.sa-toc{position:sticky;top:var(--sa-toc-top,88px);margin-bottom:0}}
|
|
157
160
|
.sa-toc>summary{list-style:none;font-weight:700;font-size:.95rem;margin:0 0 .75rem;display:flex;align-items:center;justify-content:space-between;gap:.5rem;color:inherit;cursor:pointer}
|
|
158
161
|
.sa-toc>summary::-webkit-details-marker{display:none}
|
|
159
162
|
.sa-toc>summary .sa-toc-chev{transition:transform .2s ease;color:var(--sa-muted,#94a3b8)}
|
package/package.json
CHANGED