Integrate knowledge docs and admin updates
This commit is contained in:
parent
5e26b9b80f
commit
bb7c54e9ca
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
263
admin/admin.css
263
admin/admin.css
|
|
@ -802,6 +802,15 @@ body[data-admin-mode="seo"] .json-details {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body[data-admin-mode="knowledge"] #block-form,
|
||||||
|
body[data-admin-mode="knowledge"] #empty-state {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not([data-admin-mode="knowledge"]) #knowledge-form {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
@ -1669,6 +1678,221 @@ select {
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.knowledge-editor {
|
||||||
|
display: grid;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0 0.2rem 5rem;
|
||||||
|
scrollbar-color: rgba(255, 255, 255, 0.25) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-tree {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.28rem;
|
||||||
|
padding-inline: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-tree-item,
|
||||||
|
.knowledge-tree-children {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.22rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-tree-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
min-height: 3.1rem;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.28rem;
|
||||||
|
border-radius: var(--launcher-radius-circle);
|
||||||
|
background: rgba(255, 255, 255, 0.035);
|
||||||
|
padding: 0.24rem 0.34rem 0.24rem calc(0.34rem + (var(--depth, 0) * 0.82rem));
|
||||||
|
opacity: 0.86;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-tree-row[data-active="true"] {
|
||||||
|
background: rgba(255, 255, 255, 0.14);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-tree-row[data-disabled="true"] {
|
||||||
|
opacity: 0.42;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-tree-select {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2.2rem minmax(0, 1fr);
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 2.55rem;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
border-radius: var(--launcher-radius-circle);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding: 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-tree-icon {
|
||||||
|
display: grid;
|
||||||
|
width: 2.2rem;
|
||||||
|
height: 2.2rem;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: var(--launcher-radius-circle);
|
||||||
|
background: rgba(255, 255, 255, 0.055);
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 850;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-tree-copy {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-tree-title,
|
||||||
|
.knowledge-tree-meta {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-tree-title {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 850;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-tree-meta {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 720;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-tree-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 0.18rem;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-tree-row:hover .knowledge-tree-actions,
|
||||||
|
.knowledge-tree-row[data-active="true"] .knowledge-tree-actions {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-tree-action {
|
||||||
|
display: grid;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: var(--launcher-radius-circle);
|
||||||
|
background: rgba(255, 255, 255, 0.07);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding: 0;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-tree-action:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.14);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-tree-action .trash-can-icon {
|
||||||
|
width: 0.92rem;
|
||||||
|
height: 0.92rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-empty-tree {
|
||||||
|
margin-inline: calc(var(--admin-nav-pad) + 0.35rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-rich-panel {
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-rich-toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-rich-toolbar button {
|
||||||
|
min-height: 2.15rem;
|
||||||
|
border-radius: var(--launcher-radius-circle);
|
||||||
|
background: rgba(255, 255, 255, 0.075);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding: 0 0.72rem;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 850;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-rich-toolbar button:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.14);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-rich-editor {
|
||||||
|
min-height: 28rem;
|
||||||
|
overflow: auto;
|
||||||
|
border-radius: var(--launcher-radius-card);
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding: 1.1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-rich-editor:focus {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-rich-editor h2,
|
||||||
|
.knowledge-rich-editor h3,
|
||||||
|
.knowledge-rich-editor strong {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-rich-editor h2 {
|
||||||
|
margin: 1.4rem 0 0.6rem;
|
||||||
|
font-size: 1.85rem;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-rich-editor h3 {
|
||||||
|
margin: 1.2rem 0 0.55rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-rich-editor p {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-rich-editor img {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 0.65rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-rich-editor .knowledge-button {
|
||||||
|
display: inline-flex;
|
||||||
|
min-height: 2.75rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
color: rgba(8, 8, 10, 0.96);
|
||||||
|
padding: 0 1.25rem;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
font-weight: 850;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
min-height: 1.25rem;
|
min-height: 1.25rem;
|
||||||
margin: 0.8rem 0.35rem 0;
|
margin: 0.8rem 0.35rem 0;
|
||||||
|
|
@ -2469,18 +2693,57 @@ select {
|
||||||
width: min(28rem, calc(100vw - 2.8rem));
|
width: min(28rem, calc(100vw - 2.8rem));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.knowledge-button-modal-layer {
|
||||||
|
z-index: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-button-modal {
|
||||||
|
width: min(34rem, calc(100vw - 2.8rem));
|
||||||
|
}
|
||||||
|
|
||||||
.media-action-head {
|
.media-action-head {
|
||||||
padding-right: 1.25rem;
|
padding-right: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.knowledge-button-head {
|
||||||
|
padding-right: 4.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.media-action-body {
|
.media-action-body {
|
||||||
padding: 0 1.25rem 1.25rem;
|
padding: 0 1.25rem 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.knowledge-button-body {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.9rem;
|
||||||
|
padding: 0 1.25rem 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-button-hint,
|
||||||
|
.knowledge-button-error {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 650;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-button-hint {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-button-error {
|
||||||
|
min-height: 1.1rem;
|
||||||
|
color: rgba(255, 123, 108, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
.media-action-foot {
|
.media-action-foot {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.knowledge-button-foot {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
.media-modal-foot {
|
.media-modal-foot {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1109
admin/admin.js
1109
admin/admin.js
File diff suppressed because it is too large
Load Diff
|
|
@ -27,6 +27,9 @@
|
||||||
<button id="admin-mode-seo" class="nodedc-expanded-nav-button" type="button" data-active="false">
|
<button id="admin-mode-seo" class="nodedc-expanded-nav-button" type="button" data-active="false">
|
||||||
<span>SEO</span>
|
<span>SEO</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button id="admin-mode-knowledge" class="nodedc-expanded-nav-button" type="button" data-active="false">
|
||||||
|
<span>База знаний</span>
|
||||||
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<div class="nodedc-expanded-toolbar-right">
|
<div class="nodedc-expanded-toolbar-right">
|
||||||
|
|
@ -113,8 +116,9 @@
|
||||||
<input id="block-template" name="template" type="text" autocomplete="off" readonly />
|
<input id="block-template" name="template" type="text" autocomplete="off" readonly />
|
||||||
</label>
|
</label>
|
||||||
<label class="field-control">
|
<label class="field-control">
|
||||||
<span>Anchor</span>
|
<span>Target</span>
|
||||||
<input id="block-anchor" name="anchor" type="text" autocomplete="off" />
|
<input id="block-anchor" name="target" type="text" autocomplete="off" list="block-target-options" />
|
||||||
|
<datalist id="block-target-options"></datalist>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
@ -174,6 +178,8 @@
|
||||||
</details>
|
</details>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div id="knowledge-form" class="knowledge-editor hidden"></div>
|
||||||
|
|
||||||
<pre id="status" class="status"></pre>
|
<pre id="status" class="status"></pre>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
@ -300,6 +306,39 @@
|
||||||
</footer>
|
</footer>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="knowledge-button-modal" class="template-modal-layer knowledge-button-modal-layer hidden" role="dialog" aria-modal="true" aria-labelledby="knowledge-button-title">
|
||||||
|
<article class="template-modal knowledge-button-modal">
|
||||||
|
<header class="template-modal-head knowledge-button-head">
|
||||||
|
<div>
|
||||||
|
<div class="section-kicker">База знаний</div>
|
||||||
|
<h2 id="knowledge-button-title">Кнопка в статье</h2>
|
||||||
|
<p>Ссылка ведёт на лендинг или внешний адрес. Target открывает нужный блок лендинга.</p>
|
||||||
|
</div>
|
||||||
|
<button id="knowledge-button-close" class="admin-panel-close modal-close-button" type="button" aria-label="Закрыть">×</button>
|
||||||
|
</header>
|
||||||
|
<div class="knowledge-button-body">
|
||||||
|
<label class="field-control">
|
||||||
|
<span>Название</span>
|
||||||
|
<input id="knowledge-button-name" type="text" autocomplete="off" placeholder="Посмотреть демо" />
|
||||||
|
</label>
|
||||||
|
<label class="field-control">
|
||||||
|
<span>Ссылка</span>
|
||||||
|
<input id="knowledge-button-link" type="text" autocomplete="off" placeholder="/" />
|
||||||
|
</label>
|
||||||
|
<label class="field-control">
|
||||||
|
<span>Target / блок лендинга</span>
|
||||||
|
<input id="knowledge-button-target" type="text" autocomplete="off" list="knowledge-button-target-options" placeholder="hub-video" />
|
||||||
|
<datalist id="knowledge-button-target-options"></datalist>
|
||||||
|
</label>
|
||||||
|
<p class="knowledge-button-hint">Можно выбрать target из списка или ввести ID блока лендинга. Если target пустой, ссылка откроет первый экран.</p>
|
||||||
|
<p id="knowledge-button-error" class="knowledge-button-error" aria-live="polite"></p>
|
||||||
|
</div>
|
||||||
|
<footer class="template-modal-foot knowledge-button-foot">
|
||||||
|
<button id="knowledge-button-cancel" class="ghost-btn" type="button">Отмена</button>
|
||||||
|
<button id="knowledge-button-confirm" class="primary-btn" type="button">Добавить кнопку</button>
|
||||||
|
</footer>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
<script src="./admin.js" type="module"></script>
|
<script src="./admin.js" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
.site-title-logo:where(.astro-idrpryed) img {
|
||||||
|
display: block;
|
||||||
|
width: auto;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-search-popover {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 0.5rem);
|
||||||
|
left: 50%;
|
||||||
|
z-index: calc(var(--sl-z-index-navbar) + 1);
|
||||||
|
width: min(28rem, calc(100vw - 2rem));
|
||||||
|
max-height: calc(100vh - 7rem);
|
||||||
|
overflow: auto;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
border: 1px solid var(--sl-color-gray-5);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background: var(--sl-color-gray-6);
|
||||||
|
box-shadow: var(--sl-shadow-lg);
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-search-popover[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-download-placeholder {
|
||||||
|
visibility: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-download-placeholder:where(.astro-sle7k2hz) {
|
||||||
|
min-width: 8.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-search-input {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid var(--sl-color-gray-5);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background: var(--sl-color-black);
|
||||||
|
color: var(--sl-color-white);
|
||||||
|
font: inherit;
|
||||||
|
padding: 0.75rem 0.875rem;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-search-results {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.25rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-search-result {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.2rem;
|
||||||
|
border-radius: 0.35rem;
|
||||||
|
color: var(--sl-color-gray-2);
|
||||||
|
padding: 0.65rem 0.7rem;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-search-result:hover {
|
||||||
|
background: var(--sl-color-gray-5);
|
||||||
|
color: var(--sl-color-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-search-result strong {
|
||||||
|
color: var(--sl-color-white);
|
||||||
|
font-size: var(--sl-text-sm);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-search-result span {
|
||||||
|
color: var(--sl-color-gray-3);
|
||||||
|
font-size: var(--sl-text-xs);
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary:where(.astro-j63poqv6) .knowledge-summary-link {
|
||||||
|
display: inline;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary:where(.astro-j63poqv6)[aria-current="page"] .knowledge-summary-link,
|
||||||
|
summary:where(.astro-j63poqv6)[aria-current="page"] .large,
|
||||||
|
summary:where(.astro-j63poqv6)[aria-current="page"] .caret {
|
||||||
|
color: var(--sl-color-text-invert);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sl-markdown-content .knowledge-button:not(:where(.not-content *)) {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid var(--sl-color-text-accent);
|
||||||
|
border-radius: 999rem;
|
||||||
|
background: var(--sl-color-text-accent);
|
||||||
|
color: var(--sl-color-black);
|
||||||
|
font-size: var(--sl-text-base);
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.1875;
|
||||||
|
padding: 0.9375rem 1.25rem;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sl-markdown-content .knowledge-button:hover:not(:where(.not-content *)) {
|
||||||
|
color: var(--sl-color-black);
|
||||||
|
filter: brightness(1.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-card-list {
|
||||||
|
padding-inline-start: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-card-list li + li {
|
||||||
|
margin-top: 0.35rem;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
import{S as r}from"./TableOfContents.astro_astro_type_script_index_0_lang.FuRcXuRY.js";class c extends r{set current(e){super.current=e;const t=this.querySelector(".display-current");t&&(t.textContent=e.textContent)}constructor(){super();const e=this.querySelector("details");if(!e)return;const t=()=>{e.open=!1};e.querySelectorAll("a").forEach(s=>{s.addEventListener("click",t)}),window.addEventListener("click",s=>{e.contains(s.target)||t()}),window.addEventListener("keydown",s=>{if(s.key==="Escape"&&e.open){const o=e.contains(document.activeElement);if(t(),o){const n=e.querySelector("summary");n&&n.focus()}}})}}customElements.define("mobile-starlight-toc",c);
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
const m="_top";class H extends HTMLElement{constructor(){super(),this._current=this.querySelector('a[aria-current="true"]'),this.minH=parseInt(this.dataset.minH||"2",10),this.maxH=parseInt(this.dataset.maxH||"3",10),this.tocHeadingSelector=`h1#${m},:where(${[...Array.from({length:1+this.maxH-this.minH}).map((e,n)=>`h${this.minH+n}`)].join()})[id]`,this.onIdle=e=>(window.requestIdleCallback||(n=>setTimeout(n,1)))(e),this.init=()=>{const e=[...this.querySelectorAll("a")],n=t=>t.matches(this.tocHeadingSelector),i=t=>{if(!t)return null;const h=t;for(;t;){if(t.matches(".sl-markdown-content, main > *"))return document.getElementById(m);if(n(t))return t;const s=t.querySelector(this.tocHeadingSelector);if(s)return s;for(t=t.previousElementSibling;t?.lastElementChild;)t=t.lastElementChild;const o=i(t);if(o)return o}return i(h.parentElement)},c=t=>{for(const{isIntersecting:h,target:s}of t){if(!h)continue;const o=i(s);if(!o)continue;const l=e.find(g=>g.hash==="#"+encodeURIComponent(o.id));if(l){this.current=l;break}}},a=document.querySelectorAll([`main :where(${this.tocHeadingSelector})`,`main :where(${this.tocHeadingSelector}, .sl-heading-wrapper) ~ *:not(:has(${this.tocHeadingSelector}))`,`main .sl-markdown-content > *:not(:has(${this.tocHeadingSelector}))`,`main > *:not(:has(${this.tocHeadingSelector}))`].join());let r;const u=()=>{r||(r=new IntersectionObserver(c,{rootMargin:this.getRootMargin()}),a.forEach(t=>r.observe(t)))};u();let d;window.addEventListener("resize",()=>{r&&(r.disconnect(),r=void 0),clearTimeout(d),d=setTimeout(()=>this.onIdle(u),200)})},this.onIdle(()=>this.init())}set current(e){e!==this._current&&(this._current&&this._current.removeAttribute("aria-current"),e.setAttribute("aria-current","true"),this._current=e)}getRootMargin(){const e=document.querySelector("header")?.getBoundingClientRect().height||0,n=this.querySelector("summary")?.getBoundingClientRect().height||0,i=e+n+32,c=i+53,a=document.documentElement.clientHeight;return`-${i}px 0% ${c-a}px`}}customElements.define("starlight-toc",H);export{H as S};
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,15 @@
|
||||||
|
<svg width="82" height="82" viewBox="0 0 82 82" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="81.216" height="81.2168" rx="19.1291" fill="url(#paint0_linear_106_796)"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M69.54 25.5803C69.54 27.0895 68.3166 28.313 66.8073 28.313H57.473C55.9637 28.313 54.7402 29.5365 54.7402 31.0457V33.7775C54.7402 35.2868 55.9637 36.5103 57.473 36.5103H66.8054C68.3146 36.5103 69.5381 37.7337 69.5381 39.243V46.7305L69.5332 46.7354V50.175C69.5332 51.6842 68.3097 52.9077 66.8005 52.9077H43.3343C41.825 52.9077 40.6016 54.1312 40.6016 55.6404V58.3722C40.6016 59.8815 41.825 61.105 43.3343 61.105H66.8073C68.3166 61.105 69.54 62.3285 69.54 63.8377V66.5705C69.54 68.0797 68.3166 69.3032 66.8073 69.3032H28.8636C27.3543 69.3032 26.1309 68.0797 26.1309 66.5705V63.8387C26.1309 62.3294 24.9074 61.106 23.3981 61.106H14.3929C12.8836 61.106 11.6602 59.8825 11.6602 58.3732V55.6404C11.6602 54.1312 12.8836 52.9077 14.3929 52.9077H16.1462C17.6554 52.9077 18.8789 51.6842 18.8789 50.175V49.827C18.8789 48.3177 17.6554 47.0942 16.1462 47.0942H14.3929C12.8836 47.0942 11.6602 45.8708 11.6602 44.3615V28.8387C11.6602 27.3294 12.8836 26.106 14.3929 26.106H29.5925C31.1017 26.106 32.3252 24.8825 32.3252 23.3732V22.8475C32.3252 21.3382 31.1017 20.1147 29.5925 20.1147H14.3929C12.8836 20.1147 11.6602 18.8913 11.6602 17.382V14.6492C11.6602 13.14 12.8836 11.9165 14.3929 11.9165H40.9059C42.4152 11.9165 43.6387 13.14 43.6387 14.6492V17.382C43.6387 18.8913 44.8622 20.1147 46.3714 20.1147H66.8073C68.3166 20.1147 69.54 21.3382 69.54 22.8475V25.5803ZM33.5186 41.9767C33.5186 43.486 34.742 44.7095 36.2513 44.7095H44.947C46.4562 44.7095 47.6797 43.486 47.6797 41.9767V39.244C47.6797 37.7347 46.4562 36.5112 44.947 36.5112H36.2513C34.742 36.5112 33.5186 37.7347 33.5186 39.244V41.9767Z" fill="#FDFDFD"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M69.54 25.5803C69.54 27.0895 68.3166 28.313 66.8073 28.313H57.473C55.9637 28.313 54.7402 29.5365 54.7402 31.0457V33.7775C54.7402 35.2868 55.9637 36.5103 57.473 36.5103H66.8054C68.3146 36.5103 69.5381 37.7337 69.5381 39.243V46.7305L69.5332 46.7354V50.175C69.5332 51.6842 68.3097 52.9077 66.8005 52.9077H43.3343C41.825 52.9077 40.6016 54.1312 40.6016 55.6404V58.3722C40.6016 59.8815 41.825 61.105 43.3343 61.105H66.8073C68.3166 61.105 69.54 62.3285 69.54 63.8377V66.5705C69.54 68.0797 68.3166 69.3032 66.8073 69.3032H28.8636C27.3543 69.3032 26.1309 68.0797 26.1309 66.5705V63.8387C26.1309 62.3294 24.9074 61.106 23.3981 61.106H14.3929C12.8836 61.106 11.6602 59.8825 11.6602 58.3732V55.6404C11.6602 54.1312 12.8836 52.9077 14.3929 52.9077H16.1462C17.6554 52.9077 18.8789 51.6842 18.8789 50.175V49.827C18.8789 48.3177 17.6554 47.0942 16.1462 47.0942H14.3929C12.8836 47.0942 11.6602 45.8708 11.6602 44.3615V28.8387C11.6602 27.3294 12.8836 26.106 14.3929 26.106H29.5925C31.1017 26.106 32.3252 24.8825 32.3252 23.3732V22.8475C32.3252 21.3382 31.1017 20.1147 29.5925 20.1147H14.3929C12.8836 20.1147 11.6602 18.8913 11.6602 17.382V14.6492C11.6602 13.14 12.8836 11.9165 14.3929 11.9165H40.9059C42.4152 11.9165 43.6387 13.14 43.6387 14.6492V17.382C43.6387 18.8913 44.8622 20.1147 46.3714 20.1147H66.8073C68.3166 20.1147 69.54 21.3382 69.54 22.8475V25.5803ZM33.5186 41.9767C33.5186 43.486 34.742 44.7095 36.2513 44.7095H44.947C46.4562 44.7095 47.6797 43.486 47.6797 41.9767V39.244C47.6797 37.7347 46.4562 36.5112 44.947 36.5112H36.2513C34.742 36.5112 33.5186 37.7347 33.5186 39.244V41.9767Z" fill="url(#paint1_linear_106_796)" fill-opacity="0.2"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_106_796" x1="40.608" y1="0" x2="40.608" y2="81.2168" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#1B1B1B"/>
|
||||||
|
<stop offset="1"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint1_linear_106_796" x1="40.6001" y1="11.9165" x2="40.6001" y2="69.3032" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="white"/>
|
||||||
|
<stop offset="1" stop-color="#666666"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.8 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
const v={},d=new Set,s=new WeakSet;let f=!0,h,l=!1;function g(e){l||(l=!0,f??=!1,h??="hover",p(),w(),y(),S())}function p(){for(const e of["touchstart","mousedown"])document.addEventListener(e,t=>{a(t.target,"tap")&&c(t.target.href,{ignoreSlowConnection:!0})},{passive:!0})}function w(){let e;document.body.addEventListener("focusin",n=>{a(n.target,"hover")&&t(n)},{passive:!0}),document.body.addEventListener("focusout",o,{passive:!0}),u(()=>{for(const n of document.getElementsByTagName("a"))s.has(n)||a(n,"hover")&&(s.add(n),n.addEventListener("mouseenter",t,{passive:!0}),n.addEventListener("mouseleave",o,{passive:!0}))});function t(n){const r=n.target.href;e&&clearTimeout(e),e=setTimeout(()=>{c(r)},80)}function o(){e&&(clearTimeout(e),e=0)}}function y(){let e;u(()=>{for(const t of document.getElementsByTagName("a"))s.has(t)||a(t,"viewport")&&(s.add(t),e??=L(),e.observe(t))})}function L(){const e=new WeakMap;return new IntersectionObserver((t,o)=>{for(const n of t){const r=n.target,i=e.get(r);n.isIntersecting?(i&&clearTimeout(i),e.set(r,setTimeout(()=>{o.unobserve(r),e.delete(r),c(r.href)},300))):i&&(clearTimeout(i),e.delete(r))}})}function S(){u(()=>{for(const e of document.getElementsByTagName("a"))a(e,"load")&&c(e.href)})}function c(e,t){e=e.replace(/#.*/,"");const o=t?.ignoreSlowConnection??!1;if(E(e,o))if(d.add(e),document.createElement("link").relList?.supports?.("prefetch")&&t?.with!=="fetch"){const n=document.createElement("link");n.rel="prefetch",n.setAttribute("href",e),document.head.append(n)}else{const n=new Headers;for(const[r,i]of Object.entries(v))n.set(r,i);fetch(e,{priority:"low",headers:n})}}function E(e,t){if(!navigator.onLine||!t&&m())return!1;try{const o=new URL(e,location.href);return location.origin===o.origin&&(location.pathname!==o.pathname||location.search!==o.search)&&!d.has(e)}catch{}return!1}function a(e,t){if(e?.tagName!=="A")return!1;const o=e.dataset.astroPrefetch;return o==="false"?!1:t==="tap"&&(o!=null||f)&&m()?!0:o==null&&f||o===""?t===h:o===t}function m(){if("connection"in navigator){const e=navigator.connection;return e.saveData||/2g/.test(e.effectiveType)}return!1}function u(e){e();let t=!1;document.addEventListener("astro:page-load",()=>{if(!t){t=!0;return}e()})}g();
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
@media print{:root{--sl-color-white: hsl(224, 10%, 10%);--sl-color-gray-1: hsl(224, 14%, 16%);--sl-color-gray-2: hsl(224, 10%, 23%);--sl-color-gray-3: hsl(224, 7%, 36%);--sl-color-gray-4: hsl(224, 6%, 56%);--sl-color-gray-5: hsl(224, 6%, 77%);--sl-color-gray-6: hsl(224, 20%, 94%);--sl-color-gray-7: hsl(224, 19%, 97%);--sl-color-black: hsl(0, 0%, 100%);--sl-color-orange-high: hsl(var(--sl-hue-orange), 80%, 25%);--sl-color-orange: hsl(var(--sl-hue-orange), 90%, 60%);--sl-color-orange-low: hsl(var(--sl-hue-orange), 90%, 88%);--sl-color-green-high: hsl(var(--sl-hue-green), 80%, 22%);--sl-color-green: hsl(var(--sl-hue-green), 90%, 46%);--sl-color-green-low: hsl(var(--sl-hue-green), 85%, 90%);--sl-color-blue-high: hsl(var(--sl-hue-blue), 80%, 30%);--sl-color-blue: hsl(var(--sl-hue-blue), 90%, 60%);--sl-color-blue-low: hsl(var(--sl-hue-blue), 88%, 90%);--sl-color-purple-high: hsl(var(--sl-hue-purple), 90%, 30%);--sl-color-purple: hsl(var(--sl-hue-purple), 90%, 60%);--sl-color-purple-low: hsl(var(--sl-hue-purple), 80%, 90%);--sl-color-red-high: hsl(var(--sl-hue-red), 80%, 30%);--sl-color-red: hsl(var(--sl-hue-red), 90%, 60%);--sl-color-red-low: hsl(var(--sl-hue-red), 80%, 90%);--sl-color-accent-high: hsl(234, 80%, 30%);--sl-color-accent: hsl(234, 90%, 60%);--sl-color-accent-low: hsl(234, 88%, 90%);--sl-color-text-accent: var(--sl-color-accent);--sl-color-text-invert: var(--sl-color-black);--sl-color-bg-nav: var(--sl-color-gray-7);--sl-color-bg-sidebar: var(--sl-color-bg);--sl-color-bg-inline-code: var(--sl-color-gray-6);--sl-color-bg-accent: var(--sl-color-accent);--sl-color-hairline-light: var(--sl-color-gray-6);--sl-color-hairline-shade: var(--sl-color-gray-6);--sl-color-backdrop-overlay: hsla(225, 9%, 36%, .66);--sl-shadow-sm: none;--sl-shadow-md: none;--sl-shadow-lg: none}.print\:hidden{display:none}.print\:flex{display:flex}.print\:block{display:block}main{padding-bottom:0}main>.content-panel{padding-block-start:0}.content-panel+.content-panel{border:0}.page>header{position:relative}.page>.main-frame{padding-top:0;padding-inline-start:0}.main-pane{--sl-sidebar-width: 0px;--sl-content-width: 100%}.sl-banner{--sl-color-banner-text: var(--sl-color-white);background-color:transparent}.sl-markdown-content :is(h1,h2,h3,h4,h5,h6){break-after:avoid}.sl-markdown-content :is(p,li){orphans:2;widows:2}.sl-markdown-content pre{overflow-x:hidden;white-space:pre-wrap}.sl-markdown-content .expressive-code,.sl-markdown-content figure,.sl-markdown-content pre{break-inside:avoid}.expressive-code .frame.is-terminal .header:before{box-shadow:inset 99rem 99rem var(--sl-color-gray-5)}.expressive-code .frame.has-title:not(.is-terminal) .header{background:transparent;border-bottom:1px solid var(--sl-color-gray-6)}.expressive-code .frame.has-title:not(.is-terminal) .title{background:transparent}.expressive-code .frame.has-title:not(.is-terminal) .title:after{border-top:0}.expressive-code .copy{display:none}.sl-markdown-content code:not(:where(.not-content *)){background-color:transparent;padding:0;margin-block:unset;font-size:.9375em}.sl-badge{background:transparent;color:var(--sl-color-white)}starlight-file-tree{break-inside:avoid}starlight-file-tree .highlight{outline:3px solid var(--sl-color-accent-low);color:var(--sl-color-text);background-color:transparent}.starlight-aside{break-inside:avoid}.sl-link-button.primary{background:transparent;border-color:var(--sl-color-white);color:var(--sl-color-white)}starlight-tabs{break-inside:avoid}.sl-steps>li:after{box-shadow:inset 99rem 99rem var(--sl-color-hairline-light)}}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M21 4.5C21 5.32843 20.3342 6 19.5128 6H17.1174C16.296 6 15.6302 6.67157 15.6302 7.5C15.6302 8.32843 16.296 9 17.1174 9H19.0171C20.1122 9 21 9.89543 21 11V13C21 14.1046 20.1122 15 19.0171 15H11.9877C11.1663 15 10.5005 15.6716 10.5005 16.5C10.5005 17.3284 11.1663 18 11.9877 18H19.5128C20.3342 18 21 18.6716 21 19.5C21 20.3284 20.3342 21 19.5128 21H6.73793C5.91657 21 5.25073 20.3284 5.25073 19.5C5.25073 18.6716 4.58488 18 3.76352 18H1.48721C0.665845 18 0 17.3284 0 16.5V16.3223C0 15.592 0.586949 15 1.31099 15C2.03503 15 2.62197 14.408 2.62197 13.6777V13.3223C2.62197 12.592 2.03503 12 1.31099 12C0.586949 12 0 11.408 0 10.6777V8C0 6.89543 0.887793 6 1.98294 6H6.01079C6.83215 6 7.49799 5.32843 7.49799 4.5C7.49799 3.67157 6.83215 3 6.01079 3H1.48721C0.665845 3 0 2.32843 0 1.5C0 0.671573 0.665845 0 1.48721 0H10.1151C10.9365 0 11.6023 0.671573 11.6023 1.5C11.6023 2.32843 12.2682 3 13.0895 3H19.5128C20.3342 3 21 3.67157 21 4.5ZM7.93079 10.5C7.93079 11.3284 8.59664 12 9.418 12H11.582C12.4034 12 13.0692 11.3284 13.0692 10.5C13.0692 9.67157 12.4034 9 11.582 9H9.418C8.59664 9 7.93079 9.67157 7.93079 10.5Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
|
|
@ -0,0 +1,201 @@
|
||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"slug": "knowledge",
|
||||||
|
"title": "Knowledge",
|
||||||
|
"basePath": "knowledge",
|
||||||
|
"settings": {
|
||||||
|
"description": "Материалы о NODE.DC, агентных модулях, интеграциях, цифровых двойниках и рабочих контурах платформы.",
|
||||||
|
"searchPlaceholder": "Поиск",
|
||||||
|
"themeDefault": "dark",
|
||||||
|
"navTitle": "Knowledge",
|
||||||
|
"downloadEnabled": false,
|
||||||
|
"homeHref": "/"
|
||||||
|
},
|
||||||
|
"introHtml": "<p>Knowledge - библиотека материалов NODE.DC о платформе, агентных контурах, интеграциях и прикладных модулях. Здесь собраны не инструкции ради инструкций, а объяснения: как устроены слои системы, где появляются данные, как они попадают в задачи и какие сценарии можно собирать поверх общей инфраструктуры.</p>\n<h2>Что здесь лежит</h2>\n<p>Раздел помогает быстро разобраться в NODE.DC как в единой операционной платформе: от Hub, Engine и Ops до тендерного агента, интеграции с 1С, цифровых двойников, беспилотников и SEO-модуля.</p>\n<h2>Как использовать</h2>\n<p>Материалы можно читать как обзор архитектуры. </p>\n<p>[button name=\"Перейти к модулям на лендинге\" link=\"/\" block=\"component-library-copy-copy-copy\" target=\"applied-modules\"]</p>",
|
||||||
|
"tree": [
|
||||||
|
{
|
||||||
|
"id": "platform-overview",
|
||||||
|
"title": "NODE.DC",
|
||||||
|
"slug": "platform",
|
||||||
|
"enabled": true,
|
||||||
|
"useContent": true,
|
||||||
|
"seo": {
|
||||||
|
"title": "NODE.DC | NODE.DC Knowledge",
|
||||||
|
"description": "Обзор платформы NODE.DC: данные, процессы, агенты, Hub, Engine, Ops и прикладные контуры предприятия."
|
||||||
|
},
|
||||||
|
"bodyHtml": "\n<p>NODE.DC - операционная платформа, которая связывает распределённые объекты, данные и действия в едином контуре управления. Она подключает физические источники, цифровые сервисы, бизнес-системы и прикладные модули, превращая разрозненные сигналы в воспроизводимые потоки событий.</p>\n<h2>Смысл платформы</h2>\n<p>В основе NODE.DC лежит модель объектов: сущности, паспорта, связи, состояния, события и допустимые действия. Эта структура даёт общую картину того, что происходит в компании, где это происходит, с чем связано и какие следующие действия доступны.</p>\n<h2>Три базовых слоя</h2>\n<ul><li><strong>Hub</strong> отвечает за вход, роли, группы, рабочие пространства и каталог приложений.</li><li><strong>Engine</strong> собирает процессы, интеграции, обработчики данных, интерфейсы и агентные сценарии.</li><li><strong>Ops</strong> принимает задачи, статусы, отчёты, согласования и результаты работы модулей.</li></ul>\n<h2>Агентный интеллектуальный слой</h2>\n<p>Поверх данных работает AIS - агентный слой платформы. Он анализирует историю и текущее состояние, выявляет отклонения, формирует объяснения, предлагает действия и запускает сценарии строго в рамках прав доступа и контекста.</p>\n<h2>Где применяется</h2>\n<p>NODE.DC полезен там, где важно быстро перейти от наблюдения к действию: в закупках, проектном офисе, инженерных данных, производстве, строительстве, цифровых двойниках, робототехнике и интеграциях с учётными системами.</p>\n<p>[button name=\"Посмотреть платформу на лендинге\" link=\"/\" block=\"ai-modes\" target=\"platform-composition\"]</p>\n",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "hub",
|
||||||
|
"title": "Hub",
|
||||||
|
"slug": "hub",
|
||||||
|
"enabled": true,
|
||||||
|
"useContent": true,
|
||||||
|
"seo": {
|
||||||
|
"title": "Hub | NODE.DC Knowledge",
|
||||||
|
"description": "NODE.DC Hub: единая точка входа, каталог приложений, роли, группы, доступы и рабочие пространства."
|
||||||
|
},
|
||||||
|
"bodyHtml": "<p><img src=\"./assets/uploads/media/ai-modes/ndc_hub_base-mqv6lm07.mp4\" alt=\"\" loading=\"lazy\"></p><p>NODE.DC Hub - единая точка доступа к среде NODE.DC. Он показывает пользователю доступные приложения, рабочие пространства и сервисы платформы с учётом роли, группы и настроек конкретного контура.</p>\n<h2>Что делает Hub</h2>\n<ul><li>собирает каталог приложений и микромодулей в одном интерфейсе;</li><li>управляет ролями, группами, отделами и правами доступа;</li><li>разделяет рабочие контуры для инженерных, финансовых, юридических, закупочных и производственных команд;</li><li>даёт администраторам понятную точку настройки платформы.</li></ul>\n<h2>Enterprise-сценарий</h2>\n<p>В корпоративном контуре администратор может назначать типовые конфигурации доступа для подразделений и отдельно расширять права конкретным пользователям. За счёт этого платформа не превращается в общий склад кнопок: каждый видит только те инструменты, которые относятся к его работе.</p>\n<h2>Связь с другими слоями</h2>\n<p>Из Hub пользователь переходит в Engine, Ops и прикладные приложения. Все переходы сохраняют контекст прав доступа, поэтому один и тот же модуль может выглядеть по-разному для руководителя проекта, инженера, закупщика или внешнего подрядчика.</p><p>[button name=\"Посмотреть демо\" link=\"http://127.0.0.1:8124/\" block=\"feature-video\" target=\"hub-video\"]</p>",
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "engine",
|
||||||
|
"title": "Engine",
|
||||||
|
"slug": "engine",
|
||||||
|
"enabled": true,
|
||||||
|
"useContent": true,
|
||||||
|
"seo": {
|
||||||
|
"title": "Engine | NODE.DC Knowledge",
|
||||||
|
"description": "NODE.DC Engine: нодовый слой для сборки процессов, интеграций, интерфейсов и агентных сценариев."
|
||||||
|
},
|
||||||
|
"bodyHtml": "<p>NODE.DC Engine - рабочий слой для проектирования и запуска процессов внутри платформы. Здесь создаются интеграции сервисов, обработчики данных, диспетчерские панели, сценарии роботизации и внутренние автоматизации для бизнеса.</p>\n<h2>Нодовый редактор</h2>\n<p>Процессы собираются в узловом редакторе. Узлы описывают источники данных, преобразования, действия, визуализацию, проверки, внешние API и пользовательские интерфейсы. Такой подход позволяет быстро добавлять новые сценарии без переписывания всей системы.</p>\n<h2>Роль ИИ-агентов</h2>\n<p>Сценарий можно собрать вручную или с помощью агента: пользователь описывает задачу, а агент предлагает архитектуру решения, добавляет необходимые узлы и готовит схему для подключения к интерфейсу.</p>\n<h2>Что можно собирать</h2>\n<ul><li>интеграции с 1С, CRM, ERP, API и webhooks;</li><li>операционные панели и диспетчерские интерфейсы;</li><li>потоки обработки документов, телеметрии и событий;</li><li>агентные цепочки для анализа, отбора, классификации и маршрутизации задач.</li></ul>\n<h2>Передача в Ops</h2>\n<p>Результат работы Engine не остаётся схемой в редакторе. Он передаётся в операционный слой: создаёт задачи, обновляет статусы, прикладывает отчёты, запускает согласования или отдаёт данные в другой модуль.</p><p>[button name=\"Посмотреть демо\" link=\"http://127.0.0.1:8124/\" block=\"feature-video-copy\" target=\"engine-video\"]</p><p><br></p>",
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ops",
|
||||||
|
"title": "Ops",
|
||||||
|
"slug": "ops",
|
||||||
|
"enabled": true,
|
||||||
|
"useContent": true,
|
||||||
|
"seo": {
|
||||||
|
"title": "Ops | NODE.DC Knowledge",
|
||||||
|
"description": "NODE.DC Ops: операционный слой для задач, статусов, отчётов, инженерных данных и результатов агентов."
|
||||||
|
},
|
||||||
|
"bodyHtml": "<p>NODE.DC Ops - операционный слой платформы. В него сводятся проекты, процессы, задачи, инженерные данные, статусы, отчёты, согласования и результаты работы прикладных контуров.</p>\n<h2>Единая рабочая среда</h2>\n<p>Ops объединяет людей, агентов и цифровые контуры предприятия. Задачи можно ставить, уточнять, декомпозировать, передавать между подразделениями, контролировать по этапам и связывать с результатами из других слоёв NODE.DC.</p>\n<h2>Ассистент внутри процесса</h2>\n<p>Сквозной ассистент работает не как справочная кнопка, а как интерфейс к рабочему контексту: задачам, проектным материалам, кодовой базе, инженерным данным, отчётам и действиям агентов. При наличии прав он может готовить материалы, создавать подзадачи и инициировать изменения в сервисах.</p>\n<h2>Форматы представления</h2>\n<ul><li>проектные дашборды и карточки задач;</li><li>календарные планы и таблицы;</li><li>графики, отчёты и рабочие области;</li><li>просмотр BIM, STEP, STL, 3DGS, LAS и других инженерных материалов.</li></ul>\n<h2>Зачем это нужно</h2>\n<p>Ops делает работу наблюдаемой: каждое действие, решение и отклонение получает место в общем контуре, а не теряется в чатах, таблицах и разрозненных файлах.</p><p>[button name=\"Посмотреть демо\" link=\"http://127.0.0.1:8124/\" block=\"feature-video-copy-copy\" target=\"ops-video\"]</p>",
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "agent-context",
|
||||||
|
"title": "Агентный контур",
|
||||||
|
"slug": "agent-context",
|
||||||
|
"enabled": true,
|
||||||
|
"useContent": true,
|
||||||
|
"seo": {
|
||||||
|
"title": "Агентный контур | NODE.DC Knowledge",
|
||||||
|
"description": "Как в NODE.DC устроена агентная автоматизация: контекст, права, задачи, данные и передача результатов между модулями."
|
||||||
|
},
|
||||||
|
"bodyHtml": "\n<p>Агентный контур NODE.DC - рабочая среда для сборки, запуска и контроля цифровых агентов внутри единой операционной платформы. Здесь агентная автоматизация не является набором отдельных скриптов: она встроена в процессы, роли, данные и задачи компании.</p>\n<h2>Что передаётся агентам</h2>\n<p>Агентам можно отдавать повторяющиеся и трудоёмкие операции: поиск, отбор, классификацию, разбор документов, подготовку отчётов, сверку данных и запуск следующих шагов. Человек остаётся владельцем решения, а агент снимает рутину и ускоряет подготовку материалов.</p>\n<h2>Контекст и права</h2>\n<p>Агент работает только в доступном ему контексте. Он видит задачи, документы, модели, сервисы и историю действий в рамках настроенных прав. Это важно для enterprise-сценариев, где один и тот же процесс может касаться разных подразделений и уровней доступа.</p>\n<h2>Передача результата</h2>\n<ul><li>результат может стать подзадачей в Ops;</li><li>может уйти в юридический, закупочный или инженерный модуль;</li><li>может запустить согласование или проверку;</li><li>может лечь в отчёт, карточку объекта или рабочий дашборд.</li></ul>\n<h2>Почему это инфраструктура</h2>\n<p>Главная ценность агентного слоя в том, что он поднимает автоматизацию с уровня отдельного инструмента до уровня рабочей инфраструктуры. События, причины, решения и действия остаются в одном контуре и могут быть проверены позже.</p>\n",
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "applied-modules",
|
||||||
|
"title": "Прикладные модули",
|
||||||
|
"slug": "modules",
|
||||||
|
"enabled": true,
|
||||||
|
"useContent": true,
|
||||||
|
"seo": {
|
||||||
|
"title": "Прикладные модули | NODE.DC Knowledge",
|
||||||
|
"description": "Прикладные модули NODE.DC: отраслевые и корпоративные контуры поверх общей платформенной инфраструктуры."
|
||||||
|
},
|
||||||
|
"bodyHtml": "\n<p>Прикладные модули NODE.DC закрывают конкретные функции предприятия: закупки, тендеры, 1С, производство, строительство, цифровые двойники, беспилотники, SEO и внутренние инструменты разработки.</p>\n<h2>Задача прикладного слоя</h2>\n<p>Смысл модулей - адаптировать платформу под специфику компании. В одном контуре можно связать учётные системы, роли, задачи, файлы, инженерные данные, агентов, отчёты и интерфейсы без разрыва между приложениями.</p>\n<h2>Что можно добавить</h2>\n<ul><li>модуль под отдельный отдел или процесс;</li><li>интеграцию с внешней системой, API, 1С, CRM или ERP;</li><li>операционный дашборд и рабочий интерфейс;</li><li>агента для поиска, анализа, сверки или подготовки материалов;</li><li>инструмент разработки и администрирования для внутренней команды.</li></ul>\n<h2>Единый контекст</h2>\n<p>Модуль не живёт отдельно. Он подключается к ролям, данным, задачам и сквозному ассистенту NODE.DC. Поэтому результат работы тендерного агента может попасть в Ops, данные из 1С могут участвовать в закупочной проверке, а инженерная модель может быть связана с задачей, отчётом и согласованием.</p>\n<p>[button name=\"Смотреть прикладные модули\" link=\"/\" block=\"component-library-copy-copy-copy\" target=\"applied-modules\"]</p>\n",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "tender-agent",
|
||||||
|
"title": "Тендерный агент",
|
||||||
|
"slug": "tender-agent",
|
||||||
|
"enabled": true,
|
||||||
|
"useContent": true,
|
||||||
|
"seo": {
|
||||||
|
"title": "Тендерный агент | NODE.DC Knowledge",
|
||||||
|
"description": "Как тендерный агент NODE.DC помогает искать закупки, анализировать требования, риски, документы и передавать задачи в смежные контуры."
|
||||||
|
},
|
||||||
|
"bodyHtml": "\n<p>Тендерный агент NODE.DC закрывает рабочий контур тендерного отдела. В одном интерфейсе объединяются поиск и агрегация закупок, первичный отбор, AI-анализ конкретных тендеров и передача материалов в смежные модули.</p>\n<h2>Что делает агент</h2>\n<ul><li>собирает и нормализует закупочный поток с тендерных площадок;</li><li>выделяет требования, ограничения, сроки, финансовые параметры и рисковые зоны;</li><li>сопоставляет условия закупки с внутренними данными компании;</li><li>готовит выжимки, чек-листы и материалы для ответственных специалистов;</li><li>передаёт задачи в юридический, закупочный или операционный контур.</li></ul>\n<h2>Поведенческий поиск</h2>\n<p>Система учитывает действия специалиста: какие закупки он открывает, какие отклоняет, по каким признакам принимает решение. На этой базе формируются устойчивые паттерны отбора, а релевантные тендеры находятся точнее.</p>\n<h2>Роль человека</h2>\n<p>Специалист тендерного отдела остаётся оператором решения. Агент сокращает ручной перебор, готовит анализ и подсвечивает риски, но финальное решение, подача и коммуникация остаются под контролем команды.</p>\n<h2>Связь с платформой</h2>\n<p>Материалы тендера могут стать задачами в Ops, данными для юридического анализа, входом для 1С-интеграции или основанием для закупочной цепочки.</p>\n<p>[button name=\"Посмотреть демо на лендинге\" link=\"/\" block=\"component-library-copy-copy-copy\" target=\"applied-modules\"]</p>\n",
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1c-integration",
|
||||||
|
"title": "Интеграция с 1С",
|
||||||
|
"slug": "1c-integration",
|
||||||
|
"enabled": true,
|
||||||
|
"useContent": true,
|
||||||
|
"seo": {
|
||||||
|
"title": "Интеграция с 1С | NODE.DC Knowledge",
|
||||||
|
"description": "Как NODE.DC связывает 1С с задачами, агентами, закупками, документами, складами и операционными процессами."
|
||||||
|
},
|
||||||
|
"bodyHtml": "\n<p>Интеграция с 1С связывает учётный контур предприятия с задачами, агентами и рабочими модулями NODE.DC. 1С остаётся источником фактов, а NODE.DC добавляет операционный, аналитический и агентный слой поверх этих данных.</p>\n<h2>Какие данные подключаются</h2>\n<ul><li>справочники, контрагенты, номенклатура и статусы;</li><li>документы, счета, акты, заявки и закупочные позиции;</li><li>остатки, движение товаров, складские события и производственные данные;</li><li>связанные задачи, согласования и внутренние комментарии.</li></ul>\n<h2>Типовые сценарии</h2>\n<p>Команда может использовать данные из 1С для сверки тендерных требований, контроля закупок, подготовки отчётов, запуска согласований, проверки остатков, анализа поставщиков и передачи результата в Ops.</p>\n<h2>1С агент</h2>\n<p>1С агент помогает не просто показать данные, а ответить на рабочий вопрос: найти документ, объяснить статус, собрать выборку, подготовить задачу или подсказать следующий шаг. При этом действия выполняются в рамках прав доступа и фиксируются в операционном контуре.</p>\n<h2>Почему это важно</h2>\n<p>Без интеграции учётная система часто остаётся отдельным окном. NODE.DC связывает её с процессами: данные становятся частью задач, отчётов, агентов и модулей, а не отдельной ручной проверкой.</p>\n",
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "drone-management",
|
||||||
|
"title": "Управление беспилотниками",
|
||||||
|
"slug": "drone-management",
|
||||||
|
"enabled": true,
|
||||||
|
"useContent": true,
|
||||||
|
"seo": {
|
||||||
|
"title": "Управление беспилотниками | NODE.DC Knowledge",
|
||||||
|
"description": "Цифровой контур NODE.DC для беспилотных систем: телеметрия, IoT-состояния, видеопотоки, события и операторский интерфейс."
|
||||||
|
},
|
||||||
|
"bodyHtml": "\n<p>В NODE.DC может быть реализован интеграционный слой для работы с беспилотными решениями. Платформа становится цифровым контуром вокруг системы управления: собирает телеметрию, анализирует состояние IoT-компонентов, обрабатывает события и помогает оператору принимать решения.</p>\n<h2>Что контролирует платформа</h2>\n<ul><li>состояние узлов, датчиков и исполнительных компонентов;</li><li>параметры движения, маршруты и эксплуатационные ограничения;</li><li>историю событий, уведомления и отклонения;</li><li>видеопотоки, классификацию объектов и признаки нештатных ситуаций.</li></ul>\n<h2>ИИ-модули</h2>\n<p>Отдельное направление - анализ видеопотоков, оценка окружения, выявление препятствий, контроль состояния маршрута и подготовка кратких сводок для оператора.</p>\n<h2>Опыт и переносимость</h2>\n<p>Подход применим к разным автономным комплексам: курьерским решениям, гусеничным наземным платформам, распределённой технике и мониторинговым сценариям. В каждом случае NODE.DC отвечает за данные, события, наблюдаемость, интерфейс и агентное сопровождение.</p>\n<h2>Результат</h2>\n<p>Оператор видит не набор сырых сигналов, а понятную картину: что произошло, насколько это критично, какие действия доступны и какие данные уже собраны системой.</p>\n",
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "city-digital-twins",
|
||||||
|
"title": "Цифровые двойники городов",
|
||||||
|
"slug": "city-digital-twins",
|
||||||
|
"enabled": true,
|
||||||
|
"useContent": true,
|
||||||
|
"seo": {
|
||||||
|
"title": "Цифровые двойники городов | NODE.DC Knowledge",
|
||||||
|
"description": "Как NODE.DC может выступать точкой входа в городской цифровой двойник: 3DGS, карта, BIM, облака точек и единый контекст."
|
||||||
|
},
|
||||||
|
"bodyHtml": "\n<p>NODE.DC может работать как вход в цифровой двойник города или крупной территории. Пользователь получает единое пространственное окружение, где обзор, аналитика и инженерный разбор связаны геопривязкой, API и общим контекстом.</p>\n<h2>Три масштаба</h2>\n<h3>Обзорный слой</h3>\n<p>3DGS-сцены дают быстрый вход внутрь локации: прогулку от третьего лица, совместное присутствие, наблюдение других участников и естественное понимание пространства.</p>\n<h3>Городской слой</h3>\n<p>Карта местности становится навигацией и агрегатором слоёв: транспорт, экология, инфраструктура, распределённые объекты, статусы и события.</p>\n<h3>Инженерный слой</h3>\n<p>BIM-модели, облака точек и технические материалы позволяют перейти к детальному анализу объекта, конструкции, помещений и инженерных систем.</p>\n<h2>Единый маршрут пользователя</h2>\n<p>Человек может начать с общего обзора, перейти к городской аналитике, затем открыть конкретный объект и посмотреть его инженерную структуру без разрыва пространственной логики.</p>\n<h2>Связь с Ops</h2>\n<p>Любое наблюдение, отклонение или задача из цифрового двойника может быть связано с карточкой в Ops, ответственным исполнителем, сроком, материалами и историей действий.</p>\n",
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "industrial-contour",
|
||||||
|
"title": "Промышленный контур",
|
||||||
|
"slug": "industrial-contour",
|
||||||
|
"enabled": true,
|
||||||
|
"useContent": true,
|
||||||
|
"seo": {
|
||||||
|
"title": "Промышленный контур | NODE.DC Knowledge",
|
||||||
|
"description": "Цифровой двойник промышленного предприятия в NODE.DC: состояния цехов, оборудование, облака точек, BIM и эксплуатационные данные."
|
||||||
|
},
|
||||||
|
"bodyHtml": "\n<p>Промышленный контур NODE.DC помогает хранить и сопоставлять состояния производственных помещений, оборудования и инженерной инфраструктуры во времени. Это не только визуализация, а рабочий инструмент поддержки решений.</p>\n<h2>Цифровой архив состояний</h2>\n<p>Регулярная фиксация состояния предприятия позволяет сравнивать цеха, оборудование и инженерные зоны за разные периоды. На этой базе можно выявлять деформации, износ, перегрузки и отклонения.</p>\n<h2>Что входит</h2>\n<ul><li>BIM-данные, облака точек и фотореалистичные слои;</li><li>паспорта помещений, цехов и оборудования;</li><li>связь с ERP/1С, складами и производственными статусами;</li><li>карта-навигация для сотрудников и подрядчиков;</li><li>история изменений и материалы модернизации.</li></ul>\n<h2>Оптимизация процессов</h2>\n<p>Когда пространственные данные связаны с производственными узлами, легче находить причины износа, перегрузок и простоев. Решение принимается не по отдельной фотографии или таблице, а по контексту объекта.</p>\n<h2>Планирование модернизаций</h2>\n<p>Цифровой двойник фиксирует промежуточные состояния предприятия как живую историю объекта. Это снижает риск ошибок при проектировании, ремонте, реконструкции и внедрении нового оборудования.</p>\n",
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "enterprise-management",
|
||||||
|
"title": "Управление предприятием",
|
||||||
|
"slug": "enterprise-management",
|
||||||
|
"enabled": true,
|
||||||
|
"useContent": true,
|
||||||
|
"seo": {
|
||||||
|
"title": "Управление предприятием | NODE.DC Knowledge",
|
||||||
|
"description": "Ситуационный и мониторинговый режим NODE.DC для управленческого контура предприятия."
|
||||||
|
},
|
||||||
|
"bodyHtml": "\n<p>Управленческий контур NODE.DC собирает в одном рабочем пространстве финансовые показатели, производственные статусы, распределённые объекты, логистику, поставщиков, отклонения и выводы агентных систем.</p>\n<h2>Единое экранное пространство</h2>\n<p>Вместо разрозненных отчётов формируется живая картина предприятия. Каждый блок интерфейса связан с конкретным объектом, процессом, узлом данных или рабочим полем.</p>\n<h2>Что можно выводить</h2>\n<ul><li>финансовые и операционные показатели;</li><li>производственные статусы и риски;</li><li>состояние объектов, складов, техники и инфраструктуры;</li><li>логистику, поставщиков и закупочный поток;</li><li>аналитические сводки агентов и рекомендации по действиям.</li></ul>\n<h2>Не витрина, а рабочая проекция</h2>\n<p>Ценность режима в том, что он связан с действующей архитектурой процессов. Из показателя можно перейти в задачу, из задачи - в материалы, из материалов - в ответственного, а из отклонения - в сценарий реакции.</p>\n<h2>Роль руководителя</h2>\n<p>Руководитель видит не только итоговый отчёт, но и причинно-следственную цепочку: от источника события до текущего статуса и следующего действия.</p>\n",
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "construction-contour",
|
||||||
|
"title": "Строительный контур",
|
||||||
|
"slug": "construction-contour",
|
||||||
|
"enabled": true,
|
||||||
|
"useContent": true,
|
||||||
|
"seo": {
|
||||||
|
"title": "Строительный контур | NODE.DC Knowledge",
|
||||||
|
"description": "NODE.DC для строительства: BIM, полевые отчёты, подрядчики, дефекты, сроки, модели и операционный контроль."
|
||||||
|
},
|
||||||
|
"bodyHtml": "\n<p>Строительный контур NODE.DC связывает проектные данные, площадку, подрядчиков, BIM-модели, полевые отчёты, дефекты, сроки и согласования в одной рабочей системе.</p>\n<h2>Что объединяется</h2>\n<ul><li>BIM-модели, чертежи, облака точек и фотофиксация;</li><li>задачи, замечания, дефекты и исполнительная документация;</li><li>подрядчики, ответственные, сроки и этапы работ;</li><li>полевые отчёты и мобильные сценарии инспекций;</li><li>связь с закупками, складами и финансовыми контурами.</li></ul>\n<h2>Переход от модели к действию</h2>\n<p>Инженерная модель становится не отдельным файлом, а источником рабочих действий. Элемент модели можно связать с задачей, актом, замечанием, фотографией, сроком или ответственным исполнителем.</p>\n<h2>Контроль изменений</h2>\n<p>Для стройки важна история: что было запланировано, что изменилось, кто согласовал, какие материалы приложены и почему задача перешла в новый статус. Ops сохраняет эту цепочку в операционном контуре.</p>\n<h2>Агентное сопровождение</h2>\n<p>Агенты могут готовить сводки по дефектам, искать просрочки, сравнивать отчёты, собирать материалы к совещанию и подсвечивать зоны риска.</p>\n",
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "seo-module",
|
||||||
|
"title": "SEO модуль",
|
||||||
|
"slug": "seo-module",
|
||||||
|
"enabled": true,
|
||||||
|
"useContent": true,
|
||||||
|
"seo": {
|
||||||
|
"title": "SEO модуль | NODE.DC Knowledge",
|
||||||
|
"description": "SEO Farm как модуль NODE.DC: аудит сайта, Wordstat, карта ключей, редактор правок, media SEO и ручной apply через changeset."
|
||||||
|
},
|
||||||
|
"bodyHtml": "\n<p>SEO модуль NODE.DC строится как локальный редакторский контур для работы с существующими сайтами. Его задача - не автоматически публиковать тексты и не забивать страницу ключами, а дать управляемый процесс для редактора, дизайнера и SEO-специалиста.</p>\n<h2>Основной сценарий</h2>\n<ol><li>проект сканируется как папка сайта или рабочий источник;</li><li>собирается индекс страниц, секций, медиа и технических файлов;</li><li>выполняется baseline audit по структуре, заголовкам, описаниям, видимым текстам и медиа;</li><li>из текста извлекаются смыслы, seed-запросы и потенциальные интенты;</li><li>через Wordstat собираются реальные запросы и частотность;</li><li>формируется карта ключей по секциям;</li><li>редактор утверждает правки, проверяет переспам и применяет изменения через changeset.</li></ol>\n<h2>Что проверяется</h2>\n<ul><li>title, description, h1/h2 и порядок заголовков;</li><li>видимый текст, секции и смысловые дубли;</li><li>изображения, видео, posters, alt, title, aria-label и контекст вокруг медиа;</li><li>риски переспама, конкурирующие смыслы, тон и ограничения по длине.</li></ul>\n<h2>Почему не автопостинг</h2>\n<p>Модуль работает с рабочей копией текста. Исходный сайт не меняется до ручного Apply. Перед применением пользователь видит план, варианты правок, проверки качества и итоговый changeset.</p>\n<h2>Связь с Knowledge</h2>\n<p>Этот раздел Knowledge сам является хорошим кандидатом для SEO-модуля: каждая статья имеет отдельный URL, понятный заголовок, описание, структуру H2/H3 и может проходить через тот же редакторский контур.</p>\n",
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
{
|
{
|
||||||
"label": "О продукте",
|
"label": "О продукте",
|
||||||
"href": "",
|
"href": "",
|
||||||
"target": "index.html#features",
|
"target": "index.html#platform-composition",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"linkMode": "target",
|
"linkMode": "target",
|
||||||
"linkTarget": ""
|
"linkTarget": ""
|
||||||
|
|
@ -37,10 +37,18 @@
|
||||||
"linkMode": "href",
|
"linkMode": "href",
|
||||||
"linkTarget": "_blank"
|
"linkTarget": "_blank"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "Knowledge",
|
||||||
|
"href": "/knowledge/",
|
||||||
|
"target": "",
|
||||||
|
"enabled": true,
|
||||||
|
"linkMode": "href",
|
||||||
|
"linkTarget": ""
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "Тарифы",
|
"label": "Тарифы",
|
||||||
"href": "",
|
"href": "",
|
||||||
"target": "index.html#pricing-wrap",
|
"target": "index.html#pricing-intro",
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"linkMode": "target",
|
"linkMode": "target",
|
||||||
"linkTarget": ""
|
"linkTarget": ""
|
||||||
|
|
@ -760,7 +768,7 @@
|
||||||
"type": "aiModes",
|
"type": "aiModes",
|
||||||
"template": "project-composition.html",
|
"template": "project-composition.html",
|
||||||
"adminLabel": "СОСТАВ ПРОЕКТА",
|
"adminLabel": "СОСТАВ ПРОЕКТА",
|
||||||
"anchor": null,
|
"anchor": "platform-composition",
|
||||||
"content": {
|
"content": {
|
||||||
"heading": {
|
"heading": {
|
||||||
"muted": "NODE.DC",
|
"muted": "NODE.DC",
|
||||||
|
|
@ -894,7 +902,7 @@
|
||||||
"type": "featureVideo",
|
"type": "featureVideo",
|
||||||
"template": "project-video-first.html",
|
"template": "project-video-first.html",
|
||||||
"adminLabel": "HUB - PROJECT VIDEO - FIRST",
|
"adminLabel": "HUB - PROJECT VIDEO - FIRST",
|
||||||
"anchor": "features",
|
"anchor": "hub-video",
|
||||||
"content": {
|
"content": {
|
||||||
"windowTitle": "NDC_HUB.mp4",
|
"windowTitle": "NDC_HUB.mp4",
|
||||||
"videoSrc": "./assets/uploads/media/ai-modes/ndc_hub_base-mqv6lm07.mp4",
|
"videoSrc": "./assets/uploads/media/ai-modes/ndc_hub_base-mqv6lm07.mp4",
|
||||||
|
|
@ -1046,7 +1054,7 @@
|
||||||
"type": "componentLibrary",
|
"type": "componentLibrary",
|
||||||
"template": "project-video-detail.html",
|
"template": "project-video-detail.html",
|
||||||
"adminLabel": "HUB - PROJECT VIDEO - DETAIL",
|
"adminLabel": "HUB - PROJECT VIDEO - DETAIL",
|
||||||
"anchor": null,
|
"anchor": "hub-contour",
|
||||||
"content": {
|
"content": {
|
||||||
"heading": {
|
"heading": {
|
||||||
"muted": "Hub.",
|
"muted": "Hub.",
|
||||||
|
|
@ -1196,7 +1204,7 @@
|
||||||
"type": "featureVideo",
|
"type": "featureVideo",
|
||||||
"template": "project-video-first.html",
|
"template": "project-video-first.html",
|
||||||
"adminLabel": "ENGINE - PROJECT VIDEO - FIRST",
|
"adminLabel": "ENGINE - PROJECT VIDEO - FIRST",
|
||||||
"anchor": "features",
|
"anchor": "engine-video",
|
||||||
"content": {
|
"content": {
|
||||||
"windowTitle": "ENGINE_HUB.mp4",
|
"windowTitle": "ENGINE_HUB.mp4",
|
||||||
"videoSrc": "./assets/uploads/media/ai-modes/ndc_engine_base-mqv6lvyr.mp4",
|
"videoSrc": "./assets/uploads/media/ai-modes/ndc_engine_base-mqv6lvyr.mp4",
|
||||||
|
|
@ -1349,7 +1357,7 @@
|
||||||
"type": "componentLibrary",
|
"type": "componentLibrary",
|
||||||
"template": "project-video-detail.html",
|
"template": "project-video-detail.html",
|
||||||
"adminLabel": "ENGINE - PROJECT VIDEO - DETAIL",
|
"adminLabel": "ENGINE - PROJECT VIDEO - DETAIL",
|
||||||
"anchor": null,
|
"anchor": "engine-contour",
|
||||||
"content": {
|
"content": {
|
||||||
"heading": {
|
"heading": {
|
||||||
"muted": "Engine. ",
|
"muted": "Engine. ",
|
||||||
|
|
@ -1500,7 +1508,7 @@
|
||||||
"type": "featureVideo",
|
"type": "featureVideo",
|
||||||
"template": "project-video-first.html",
|
"template": "project-video-first.html",
|
||||||
"adminLabel": "OPS - PROJECT VIDEO - FIRST",
|
"adminLabel": "OPS - PROJECT VIDEO - FIRST",
|
||||||
"anchor": "features",
|
"anchor": "ops-video",
|
||||||
"content": {
|
"content": {
|
||||||
"windowTitle": "OPS_HUB.mp4",
|
"windowTitle": "OPS_HUB.mp4",
|
||||||
"videoSrc": "./assets/uploads/media/ai-modes/ndc_ops_blur-mqv6ss6k.mp4",
|
"videoSrc": "./assets/uploads/media/ai-modes/ndc_ops_blur-mqv6ss6k.mp4",
|
||||||
|
|
@ -1653,7 +1661,7 @@
|
||||||
"type": "componentLibrary",
|
"type": "componentLibrary",
|
||||||
"template": "project-video-detail.html",
|
"template": "project-video-detail.html",
|
||||||
"adminLabel": "OPS- PROJECT VIDEO - DETAIL",
|
"adminLabel": "OPS- PROJECT VIDEO - DETAIL",
|
||||||
"anchor": null,
|
"anchor": "ops-contour",
|
||||||
"content": {
|
"content": {
|
||||||
"heading": {
|
"heading": {
|
||||||
"muted": "OPS",
|
"muted": "OPS",
|
||||||
|
|
@ -1804,7 +1812,7 @@
|
||||||
"type": "componentLibrary",
|
"type": "componentLibrary",
|
||||||
"template": "project-video-detail.html",
|
"template": "project-video-detail.html",
|
||||||
"adminLabel": "МОДУЛИ- PROJECT VIDEO - DETAIL",
|
"adminLabel": "МОДУЛИ- PROJECT VIDEO - DETAIL",
|
||||||
"anchor": null,
|
"anchor": "applied-modules",
|
||||||
"content": {
|
"content": {
|
||||||
"heading": {
|
"heading": {
|
||||||
"muted": "Модули. ",
|
"muted": "Модули. ",
|
||||||
|
|
@ -1975,7 +1983,7 @@
|
||||||
"type": "faqSection",
|
"type": "faqSection",
|
||||||
"template": "faq.html",
|
"template": "faq.html",
|
||||||
"adminLabel": "FAQ",
|
"adminLabel": "FAQ",
|
||||||
"anchor": null,
|
"anchor": "faq",
|
||||||
"content": {
|
"content": {
|
||||||
"heading": {
|
"heading": {
|
||||||
"muted": "Частые",
|
"muted": "Частые",
|
||||||
|
|
@ -2212,7 +2220,7 @@
|
||||||
"type": "publishCta",
|
"type": "publishCta",
|
||||||
"template": "publish-cta.html",
|
"template": "publish-cta.html",
|
||||||
"adminLabel": "Финальный CTA",
|
"adminLabel": "Финальный CTA",
|
||||||
"anchor": "hero",
|
"anchor": "final-cta",
|
||||||
"content": {
|
"content": {
|
||||||
"cta": {
|
"cta": {
|
||||||
"heading": {
|
"heading": {
|
||||||
|
|
@ -2295,7 +2303,7 @@
|
||||||
"type": "centralTextBlock",
|
"type": "centralTextBlock",
|
||||||
"template": "central-text-block.html",
|
"template": "central-text-block.html",
|
||||||
"adminLabel": "Центральный текстовый блок",
|
"adminLabel": "Центральный текстовый блок",
|
||||||
"anchor": "pricing",
|
"anchor": "pricing-intro",
|
||||||
"content": {
|
"content": {
|
||||||
"eyebrow": "Тарифы",
|
"eyebrow": "Тарифы",
|
||||||
"heading": {
|
"heading": {
|
||||||
|
|
@ -2376,7 +2384,7 @@
|
||||||
"type": "pricingTable",
|
"type": "pricingTable",
|
||||||
"template": "pricing-table.html",
|
"template": "pricing-table.html",
|
||||||
"adminLabel": "Тарифы: таблица",
|
"adminLabel": "Тарифы: таблица",
|
||||||
"anchor": "pricing-wrap",
|
"anchor": "pricing-table",
|
||||||
"content": {
|
"content": {
|
||||||
"pricing": {
|
"pricing": {
|
||||||
"windowTitle": "Тарифы.app",
|
"windowTitle": "Тарифы.app",
|
||||||
|
|
@ -2870,7 +2878,7 @@
|
||||||
"type": "screenshotCards",
|
"type": "screenshotCards",
|
||||||
"template": "screenshot-cards.html",
|
"template": "screenshot-cards.html",
|
||||||
"adminLabel": "Карточки скриншотов",
|
"adminLabel": "Карточки скриншотов",
|
||||||
"anchor": null,
|
"anchor": "screenshots",
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"content": {
|
"content": {
|
||||||
"galleryItems": [
|
"galleryItems": [
|
||||||
|
|
@ -3018,7 +3026,7 @@
|
||||||
"type": "waitlistFiles",
|
"type": "waitlistFiles",
|
||||||
"template": "request-demo-txt-files.html",
|
"template": "request-demo-txt-files.html",
|
||||||
"adminLabel": "Запрос DEMO и TXT-файлы",
|
"adminLabel": "Запрос DEMO и TXT-файлы",
|
||||||
"anchor": "waitlist",
|
"anchor": "demo-request",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"content": {
|
"content": {
|
||||||
"form": {
|
"form": {
|
||||||
|
|
@ -3536,7 +3544,7 @@
|
||||||
{
|
{
|
||||||
"label": "Лист ожидания",
|
"label": "Лист ожидания",
|
||||||
"href": "",
|
"href": "",
|
||||||
"target": "index.html#waitlist",
|
"target": "index.html#demo-request",
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"linkMode": "target",
|
"linkMode": "target",
|
||||||
"linkTarget": ""
|
"linkTarget": ""
|
||||||
|
|
@ -3544,23 +3552,23 @@
|
||||||
{
|
{
|
||||||
"label": "Тарифы",
|
"label": "Тарифы",
|
||||||
"href": "",
|
"href": "",
|
||||||
"target": "index.html#",
|
"target": "index.html#pricing-intro",
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"linkMode": "target",
|
"linkMode": "target",
|
||||||
"linkTarget": ""
|
"linkTarget": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Возможности",
|
"label": "Возможности",
|
||||||
"href": "https://nodedc.dctouch.ru/",
|
"href": "",
|
||||||
"target": "index.html#features",
|
"target": "index.html#applied-modules",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"linkMode": "href",
|
"linkMode": "target",
|
||||||
"linkTarget": ""
|
"linkTarget": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Демо",
|
"label": "Демо",
|
||||||
"href": "",
|
"href": "",
|
||||||
"target": "index.html#features",
|
"target": "index.html#hub-video",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"linkMode": "target",
|
"linkMode": "target",
|
||||||
"linkTarget": ""
|
"linkTarget": ""
|
||||||
|
|
|
||||||
24
index.html
24
index.html
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -6,6 +6,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"extract:home": "node tools/extract-home-blocks.mjs",
|
"extract:home": "node tools/extract-home-blocks.mjs",
|
||||||
"render:home": "node tools/render-home.mjs",
|
"render:home": "node tools/render-home.mjs",
|
||||||
|
"render:knowledge": "node tools/render-knowledge.mjs",
|
||||||
"admin": "node tools/admin-server.mjs"
|
"admin": "node tools/admin-server.mjs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
75
sitemap.xml
75
sitemap.xml
|
|
@ -5,4 +5,79 @@
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>1.0</priority>
|
<priority>1.0</priority>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nodedc.dctouch.ru/knowledge/</loc>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nodedc.dctouch.ru/knowledge/platform/</loc>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.7</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nodedc.dctouch.ru/knowledge/platform/hub/</loc>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nodedc.dctouch.ru/knowledge/platform/engine/</loc>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nodedc.dctouch.ru/knowledge/platform/ops/</loc>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nodedc.dctouch.ru/knowledge/agent-context/</loc>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.7</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nodedc.dctouch.ru/knowledge/modules/</loc>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.7</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nodedc.dctouch.ru/knowledge/modules/tender-agent/</loc>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nodedc.dctouch.ru/knowledge/modules/1c-integration/</loc>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nodedc.dctouch.ru/knowledge/modules/drone-management/</loc>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nodedc.dctouch.ru/knowledge/modules/city-digital-twins/</loc>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nodedc.dctouch.ru/knowledge/modules/industrial-contour/</loc>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nodedc.dctouch.ru/knowledge/modules/enterprise-management/</loc>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nodedc.dctouch.ru/knowledge/modules/construction-contour/</loc>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nodedc.dctouch.ru/knowledge/modules/seo-module/</loc>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
</urlset>
|
</urlset>
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
<section id="features" class="s h-screen py mobile-flip"><div class="app-w align-right"><div id="demo-video" data-module="app" class="app-item video" data-adaptive-video-window style="--nodedc-video-height: {{attr:content.videoHeightPx}}px;"><div data-draggable-area="" class="app-head preview"><div class="app-head-trigs"><button data-close="" aria-label="закрыть" class="circle opaque"><div class="x-icon"><div class="line full x-icon rot"></div><div class="line full x-icon"></div></div></button><div class="circle opaque"></div></div><div class="app-name-w"><div class="app-name-txt preview">{{text:content.windowTitle}}</div></div></div><div class="video-size" data-adaptive-video><video src="{{attr:content.videoSrc}}" muted="" loop="" playsinline="true" preload="none" data-lazy-video="" class="video"></video></div></div></div><div class="c"><hgroup data-module="hg"><h2 class="main-h h1"><span class="darker-40">{{text:content.heading.muted}}</span><span>{{text:content.heading.main}}</span></h2><div class="h-txt">{{html:content.introHtml}}</div><div class="txt-w"><div class="hg-par darker-40">{{text:content.features.0.eyebrow}}</div><div class="h-txt">{{html:content.features.0.bodyHtml}}</div><div class="hg-par darker-40">{{text:content.features.1.eyebrow}}</div><div class="h-txt">{{html:content.features.1.bodyHtml}}</div><div class="hg-par darker-40">{{text:content.features.2.eyebrow}}</div><div class="h-txt">{{html:content.features.2.bodyHtml}}</div><div class="hg-par darker-40">{{text:content.features.3.eyebrow}}</div><div class="h-txt">{{html:content.features.3.bodyHtml}}</div></div></hgroup></div></section>
|
<section class="s h-screen py mobile-flip"><div class="app-w align-right"><div id="demo-video" data-module="app" class="app-item video" data-adaptive-video-window style="--nodedc-video-height: {{attr:content.videoHeightPx}}px;"><div data-draggable-area="" class="app-head preview"><div class="app-head-trigs"><button data-close="" aria-label="закрыть" class="circle opaque"><div class="x-icon"><div class="line full x-icon rot"></div><div class="line full x-icon"></div></div></button><div class="circle opaque"></div></div><div class="app-name-w"><div class="app-name-txt preview">{{text:content.windowTitle}}</div></div></div><div class="video-size" data-adaptive-video><video src="{{attr:content.videoSrc}}" muted="" loop="" playsinline="true" preload="none" data-lazy-video="" class="video"></video></div></div></div><div class="c"><hgroup data-module="hg"><h2 class="main-h h1"><span class="darker-40">{{text:content.heading.muted}}</span><span>{{text:content.heading.main}}</span></h2><div class="h-txt">{{html:content.introHtml}}</div><div class="txt-w"><div class="hg-par darker-40">{{text:content.features.0.eyebrow}}</div><div class="h-txt">{{html:content.features.0.bodyHtml}}</div><div class="hg-par darker-40">{{text:content.features.1.eyebrow}}</div><div class="h-txt">{{html:content.features.1.bodyHtml}}</div><div class="hg-par darker-40">{{text:content.features.2.eyebrow}}</div><div class="h-txt">{{html:content.features.2.bodyHtml}}</div><div class="hg-par darker-40">{{text:content.features.3.eyebrow}}</div><div class="h-txt">{{html:content.features.3.bodyHtml}}</div></div></hgroup></div></section>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
const port = Number(process.env.PORT || 8090);
|
const port = Number(process.env.PORT || 8090);
|
||||||
const host = process.env.HOST || "127.0.0.1";
|
const host = process.env.HOST || "127.0.0.1";
|
||||||
const pagePath = join(repoRoot, "content", "pages", "home.json");
|
const pagePath = join(repoRoot, "content", "pages", "home.json");
|
||||||
|
const knowledgePath = join(repoRoot, "content", "knowledge.json");
|
||||||
const blockTemplatesPath = join(repoRoot, "content", "block-templates", "home.json");
|
const blockTemplatesPath = join(repoRoot, "content", "block-templates", "home.json");
|
||||||
const assetRoot = join(repoRoot, "assets");
|
const assetRoot = join(repoRoot, "assets");
|
||||||
const assetTrashRoot = join(assetRoot, ".trash");
|
const assetTrashRoot = join(assetRoot, ".trash");
|
||||||
|
|
@ -169,6 +170,45 @@ function sendJson(res, status, payload) {
|
||||||
send(res, status, `${JSON.stringify(payload, null, 2)}\n`, "application/json; charset=utf-8");
|
send(res, status, `${JSON.stringify(payload, null, 2)}\n`, "application/json; charset=utf-8");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function allPageBlocks(page) {
|
||||||
|
return (page.regions || []).flatMap((region) => region.blocks || []);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeLandingTarget(value) {
|
||||||
|
const raw = String(value || "").trim();
|
||||||
|
if (!raw || /^javascript:/i.test(raw) || /^(https?:)?\/\//i.test(raw) || /^(mailto:|tel:)/i.test(raw)) return "";
|
||||||
|
const hashValue = raw.includes("#") ? raw.slice(raw.indexOf("#") + 1) : raw;
|
||||||
|
return hashValue
|
||||||
|
.replace(/^\/+/, "")
|
||||||
|
.replace(/^index\.html#?/i, "")
|
||||||
|
.replace(/^#+/, "")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function landingTargetsForPage(page) {
|
||||||
|
return allPageBlocks(page)
|
||||||
|
.filter((block) => block.enabled !== false)
|
||||||
|
.map((block) => ({
|
||||||
|
id: block.id,
|
||||||
|
label: block.adminLabel || block.id,
|
||||||
|
template: block.template || block.type || "",
|
||||||
|
target: normalizeLandingTarget(block.anchor),
|
||||||
|
}))
|
||||||
|
.filter((item) => item.target);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateLandingTargets(page) {
|
||||||
|
const targets = new Map();
|
||||||
|
|
||||||
|
for (const item of landingTargetsForPage(page)) {
|
||||||
|
if (targets.has(item.target)) {
|
||||||
|
const first = targets.get(item.target);
|
||||||
|
throw new Error(`Target "${item.target}" уже используется в блоках "${first.label}" и "${item.label}"`);
|
||||||
|
}
|
||||||
|
targets.set(item.target, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function readBodyBuffer(req) {
|
async function readBodyBuffer(req) {
|
||||||
const chunks = [];
|
const chunks = [];
|
||||||
for await (const chunk of req) {
|
for await (const chunk of req) {
|
||||||
|
|
@ -377,12 +417,15 @@ function collectAssetRefsFromText(text) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildMediaUsage() {
|
async function buildMediaUsage() {
|
||||||
const [pageSource, indexSource] = await Promise.all([
|
const [pageSource, knowledgeSource, indexSource, knowledgeIndexSource] = await Promise.all([
|
||||||
readFile(pagePath, "utf8").catch(() => "{}"),
|
readFile(pagePath, "utf8").catch(() => "{}"),
|
||||||
|
readFile(knowledgePath, "utf8").catch(() => "{}"),
|
||||||
readFile(join(repoRoot, "index.html"), "utf8").catch(() => ""),
|
readFile(join(repoRoot, "index.html"), "utf8").catch(() => ""),
|
||||||
|
readFile(join(repoRoot, "knowledge", "index.html"), "utf8").catch(() => ""),
|
||||||
]);
|
]);
|
||||||
const pageRefs = collectAssetRefsFromValue(JSON.parse(pageSource));
|
const pageRefs = collectAssetRefsFromValue(JSON.parse(pageSource));
|
||||||
const renderedRefs = collectAssetRefsFromText(indexSource);
|
collectAssetRefsFromValue(JSON.parse(knowledgeSource), pageRefs);
|
||||||
|
const renderedRefs = collectAssetRefsFromText(`${indexSource}\n${knowledgeIndexSource}`);
|
||||||
|
|
||||||
return { pageRefs, renderedRefs };
|
return { pageRefs, renderedRefs };
|
||||||
}
|
}
|
||||||
|
|
@ -1389,14 +1432,19 @@ function safePublicPath(urlPath) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function serveStatic(req, res) {
|
async function serveStatic(req, res) {
|
||||||
const filePath = safePublicPath(new URL(req.url, `http://${host}:${port}`).pathname);
|
let filePath = safePublicPath(new URL(req.url, `http://${host}:${port}`).pathname);
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
send(res, 403, "Forbidden");
|
send(res, 403, "Forbidden");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const fileStat = await stat(filePath);
|
let fileStat = await stat(filePath);
|
||||||
|
if (fileStat.isDirectory()) {
|
||||||
|
filePath = join(filePath, "index.html");
|
||||||
|
fileStat = await stat(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
if (!fileStat.isFile()) {
|
if (!fileStat.isFile()) {
|
||||||
send(res, 404, "Not found");
|
send(res, 404, "Not found");
|
||||||
return;
|
return;
|
||||||
|
|
@ -1462,6 +1510,18 @@ const server = createServer(async (req, res) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.method === "GET" && url.pathname === "/api/knowledge") {
|
||||||
|
send(res, 200, await readFile(knowledgePath, "utf8"), "application/json; charset=utf-8");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "GET" && url.pathname === "/api/landing-targets") {
|
||||||
|
const page = JSON.parse(await readFile(pagePath, "utf8"));
|
||||||
|
validateLandingTargets(page);
|
||||||
|
sendJson(res, 200, { ok: true, targets: landingTargetsForPage(page) });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (req.method === "GET" && url.pathname === "/api/block-templates/home") {
|
if (req.method === "GET" && url.pathname === "/api/block-templates/home") {
|
||||||
send(res, 200, await readFile(blockTemplatesPath, "utf8"), "application/json; charset=utf-8");
|
send(res, 200, await readFile(blockTemplatesPath, "utf8"), "application/json; charset=utf-8");
|
||||||
return;
|
return;
|
||||||
|
|
@ -1490,17 +1550,32 @@ const server = createServer(async (req, res) => {
|
||||||
if (req.method === "PUT" && url.pathname === "/api/page/home") {
|
if (req.method === "PUT" && url.pathname === "/api/page/home") {
|
||||||
const body = await readBody(req);
|
const body = await readBody(req);
|
||||||
const parsed = JSON.parse(body);
|
const parsed = JSON.parse(body);
|
||||||
|
validateLandingTargets(parsed);
|
||||||
await writeFile(pagePath, `${JSON.stringify(parsed, null, 2)}\n`);
|
await writeFile(pagePath, `${JSON.stringify(parsed, null, 2)}\n`);
|
||||||
sendJson(res, 200, { ok: true });
|
sendJson(res, 200, { ok: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.method === "PUT" && url.pathname === "/api/knowledge") {
|
||||||
|
const body = await readBody(req);
|
||||||
|
const parsed = JSON.parse(body);
|
||||||
|
await writeFile(knowledgePath, `${JSON.stringify(parsed, null, 2)}\n`);
|
||||||
|
sendJson(res, 200, { ok: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (req.method === "POST" && url.pathname === "/api/render/home") {
|
if (req.method === "POST" && url.pathname === "/api/render/home") {
|
||||||
const result = await runNodeScript("render-home.mjs");
|
const result = await runNodeScript("render-home.mjs");
|
||||||
sendJson(res, 200, { ok: true, ...result });
|
sendJson(res, 200, { ok: true, ...result });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.method === "POST" && url.pathname === "/api/render/knowledge") {
|
||||||
|
const result = await runNodeScript("render-knowledge.mjs");
|
||||||
|
sendJson(res, 200, { ok: true, ...result });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (req.method === "POST" && url.pathname === "/api/upload/asset") {
|
if (req.method === "POST" && url.pathname === "/api/upload/asset") {
|
||||||
const result = isMultipartRequest(req)
|
const result = isMultipartRequest(req)
|
||||||
? await saveUploadedAssetBuffer(parseMultipartUpload(req, await readBodyBuffer(req)))
|
? await saveUploadedAssetBuffer(parseMultipartUpload(req, await readBodyBuffer(req)))
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
const pagePath = join(repoRoot, "content", "pages", "home.json");
|
const pagePath = join(repoRoot, "content", "pages", "home.json");
|
||||||
|
const knowledgePath = join(repoRoot, "content", "knowledge.json");
|
||||||
const robotsPath = join(repoRoot, "robots.txt");
|
const robotsPath = join(repoRoot, "robots.txt");
|
||||||
const sitemapPath = join(repoRoot, "sitemap.xml");
|
const sitemapPath = join(repoRoot, "sitemap.xml");
|
||||||
const templateRoot = join(repoRoot, "templates", "pages", "home");
|
const templateRoot = join(repoRoot, "templates", "pages", "home");
|
||||||
|
|
@ -193,6 +194,115 @@ function escapeXml(value) {
|
||||||
.replaceAll("'", "'");
|
.replaceAll("'", "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stripHtml(value) {
|
||||||
|
return String(value ?? "")
|
||||||
|
.replace(/<[^>]*>/g, " ")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeLandingTarget(value) {
|
||||||
|
const raw = String(value || "").trim();
|
||||||
|
if (!raw || /^javascript:/i.test(raw) || isExternalHref(raw)) return "";
|
||||||
|
const hashValue = raw.includes("#") ? raw.slice(raw.indexOf("#") + 1) : raw;
|
||||||
|
return hashValue
|
||||||
|
.replace(/^\/+/, "")
|
||||||
|
.replace(/^index\.html#?/i, "")
|
||||||
|
.replace(/^#+/, "")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateLandingTargets(model) {
|
||||||
|
const targets = new Map();
|
||||||
|
|
||||||
|
for (const block of allBlocks(model)) {
|
||||||
|
if (block.enabled === false) continue;
|
||||||
|
const target = normalizeLandingTarget(block.anchor);
|
||||||
|
if (!target) continue;
|
||||||
|
|
||||||
|
if (targets.has(target)) {
|
||||||
|
const first = targets.get(target);
|
||||||
|
throw new Error(
|
||||||
|
`Duplicate landing target "${target}" in blocks "${first.adminLabel || first.id}" and "${block.adminLabel || block.id}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
targets.set(target, block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function slugify(value, fallback = "section") {
|
||||||
|
const map = {
|
||||||
|
а: "a",
|
||||||
|
б: "b",
|
||||||
|
в: "v",
|
||||||
|
г: "g",
|
||||||
|
д: "d",
|
||||||
|
е: "e",
|
||||||
|
ё: "e",
|
||||||
|
ж: "zh",
|
||||||
|
з: "z",
|
||||||
|
и: "i",
|
||||||
|
й: "y",
|
||||||
|
к: "k",
|
||||||
|
л: "l",
|
||||||
|
м: "m",
|
||||||
|
н: "n",
|
||||||
|
о: "o",
|
||||||
|
п: "p",
|
||||||
|
р: "r",
|
||||||
|
с: "s",
|
||||||
|
т: "t",
|
||||||
|
у: "u",
|
||||||
|
ф: "f",
|
||||||
|
х: "h",
|
||||||
|
ц: "c",
|
||||||
|
ч: "ch",
|
||||||
|
ш: "sh",
|
||||||
|
щ: "sch",
|
||||||
|
ъ: "",
|
||||||
|
ы: "y",
|
||||||
|
ь: "",
|
||||||
|
э: "e",
|
||||||
|
ю: "yu",
|
||||||
|
я: "ya",
|
||||||
|
};
|
||||||
|
const safe = String(value ?? "")
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[а-яё]/g, (char) => map[char] ?? "")
|
||||||
|
.replace(/[^a-z0-9]+/g, "-")
|
||||||
|
.replace(/^-+|-+$/g, "");
|
||||||
|
|
||||||
|
return safe || fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectKnowledgeUrls(source, canonical) {
|
||||||
|
let model;
|
||||||
|
try {
|
||||||
|
model = JSON.parse(source);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const basePath = String(model.basePath || "knowledge").replace(/^\/+|\/+$/g, "") || "knowledge";
|
||||||
|
const urls = [new URL(`${basePath}/`, canonical).href];
|
||||||
|
|
||||||
|
function walk(nodes = [], parentSegments = []) {
|
||||||
|
nodes.forEach((node, index) => {
|
||||||
|
if (node.enabled === false) return;
|
||||||
|
const segment = slugify(node.slug || node.title || node.id, `page-${index + 1}`);
|
||||||
|
const segments = [...parentSegments, segment];
|
||||||
|
if (node.useContent !== false && stripHtml(node.bodyHtml).length > 0) {
|
||||||
|
urls.push(new URL(`${basePath}/${segments.join("/")}/`, canonical).href);
|
||||||
|
}
|
||||||
|
if (Array.isArray(node.children)) walk(node.children, segments);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
walk(model.tree || []);
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
|
|
||||||
function canonicalUrl(model) {
|
function canonicalUrl(model) {
|
||||||
const fallback = "https://nodedc.dctouch.ru/";
|
const fallback = "https://nodedc.dctouch.ru/";
|
||||||
const value = String(model.seo?.canonicalUrl || fallback).trim() || fallback;
|
const value = String(model.seo?.canonicalUrl || fallback).trim() || fallback;
|
||||||
|
|
@ -208,6 +318,8 @@ function canonicalUrl(model) {
|
||||||
async function writeSeoFiles(model) {
|
async function writeSeoFiles(model) {
|
||||||
const canonical = canonicalUrl(model);
|
const canonical = canonicalUrl(model);
|
||||||
const sitemapUrl = new URL("sitemap.xml", canonical).href;
|
const sitemapUrl = new URL("sitemap.xml", canonical).href;
|
||||||
|
const knowledgeSource = await readFile(knowledgePath, "utf8").catch(() => "");
|
||||||
|
const knowledgeUrls = collectKnowledgeUrls(knowledgeSource, canonical);
|
||||||
const robots = ["User-agent: *", "Allow: /", `Sitemap: ${sitemapUrl}`, ""].join("\n");
|
const robots = ["User-agent: *", "Allow: /", `Sitemap: ${sitemapUrl}`, ""].join("\n");
|
||||||
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
|
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
|
@ -216,6 +328,15 @@ async function writeSeoFiles(model) {
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>1.0</priority>
|
<priority>1.0</priority>
|
||||||
</url>
|
</url>
|
||||||
|
${knowledgeUrls
|
||||||
|
.map(
|
||||||
|
(url) => ` <url>
|
||||||
|
<loc>${escapeXml(url)}</loc>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.7</priority>
|
||||||
|
</url>`,
|
||||||
|
)
|
||||||
|
.join("\n")}
|
||||||
</urlset>
|
</urlset>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
@ -306,6 +427,16 @@ function scopeDuplicateIds(html, scope) {
|
||||||
return scoped;
|
return scoped;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function htmlHasId(html, id) {
|
||||||
|
return new RegExp(`\\sid="${escapeRegExp(id)}"`).test(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
function withLandingTarget(html, block) {
|
||||||
|
const target = normalizeLandingTarget(block.anchor);
|
||||||
|
if (!target || htmlHasId(html, target)) return html;
|
||||||
|
return `<span id="${escapeAttr(target)}" class="landing-target" aria-hidden="true"></span>${html}`;
|
||||||
|
}
|
||||||
|
|
||||||
async function renderBlock(block, renderCounts) {
|
async function renderBlock(block, renderCounts) {
|
||||||
if (block.enabled === false) {
|
if (block.enabled === false) {
|
||||||
return "";
|
return "";
|
||||||
|
|
@ -316,12 +447,9 @@ async function renderBlock(block, renderCounts) {
|
||||||
renderCounts.set(templateName, renderCount + 1);
|
renderCounts.set(templateName, renderCount + 1);
|
||||||
|
|
||||||
const html = renderTemplate(await readTemplate(templateName), block);
|
const html = renderTemplate(await readTemplate(templateName), block);
|
||||||
|
const rendered = renderCount === 0 && block.scopeIds !== true ? html : scopeDuplicateIds(html, block.id);
|
||||||
|
|
||||||
if (renderCount === 0 && block.scopeIds !== true) {
|
return withLandingTarget(rendered, block);
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
return scopeDuplicateIds(html, block.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderRegion(region) {
|
async function renderRegion(region) {
|
||||||
|
|
@ -367,6 +495,7 @@ async function renderSections() {
|
||||||
applySeoDefaults(page);
|
applySeoDefaults(page);
|
||||||
applySharedDemoRequest(page);
|
applySharedDemoRequest(page);
|
||||||
applyRenderableLinks(page);
|
applyRenderableLinks(page);
|
||||||
|
validateLandingTargets(page);
|
||||||
|
|
||||||
let output = renderTemplate(shell, { id: page.slug, ...page });
|
let output = renderTemplate(shell, { id: page.slug, ...page });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,769 @@
|
||||||
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||||
|
import { dirname, join } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
|
const knowledgePath = join(repoRoot, "content", "knowledge.json");
|
||||||
|
const homePath = join(repoRoot, "content", "pages", "home.json");
|
||||||
|
const robotsPath = join(repoRoot, "robots.txt");
|
||||||
|
const sitemapPath = join(repoRoot, "sitemap.xml");
|
||||||
|
|
||||||
|
const knowledge = JSON.parse(await readFile(knowledgePath, "utf8"));
|
||||||
|
const home = JSON.parse(await readFile(homePath, "utf8").catch(() => "{}"));
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value ?? "")
|
||||||
|
.replaceAll("&", "&")
|
||||||
|
.replaceAll("<", "<")
|
||||||
|
.replaceAll(">", ">");
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeAttr(value) {
|
||||||
|
return escapeHtml(value).replaceAll('"', """).replaceAll("'", "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeXml(value) {
|
||||||
|
return escapeHtml(value).replaceAll('"', """).replaceAll("'", "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeJson(value) {
|
||||||
|
return JSON.stringify(value).replaceAll("</script", "<\\/script");
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripHtml(value) {
|
||||||
|
return String(value ?? "")
|
||||||
|
.replace(/<script[\s\S]*?<\/script>/gi, " ")
|
||||||
|
.replace(/<style[\s\S]*?<\/style>/gi, " ")
|
||||||
|
.replace(/<[^>]*>/g, " ")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function slugify(value, fallback = "section") {
|
||||||
|
const map = {
|
||||||
|
а: "a",
|
||||||
|
б: "b",
|
||||||
|
в: "v",
|
||||||
|
г: "g",
|
||||||
|
д: "d",
|
||||||
|
е: "e",
|
||||||
|
ё: "e",
|
||||||
|
ж: "zh",
|
||||||
|
з: "z",
|
||||||
|
и: "i",
|
||||||
|
й: "y",
|
||||||
|
к: "k",
|
||||||
|
л: "l",
|
||||||
|
м: "m",
|
||||||
|
н: "n",
|
||||||
|
о: "o",
|
||||||
|
п: "p",
|
||||||
|
р: "r",
|
||||||
|
с: "s",
|
||||||
|
т: "t",
|
||||||
|
у: "u",
|
||||||
|
ф: "f",
|
||||||
|
х: "h",
|
||||||
|
ц: "c",
|
||||||
|
ч: "ch",
|
||||||
|
ш: "sh",
|
||||||
|
щ: "sch",
|
||||||
|
ъ: "",
|
||||||
|
ы: "y",
|
||||||
|
ь: "",
|
||||||
|
э: "e",
|
||||||
|
ю: "yu",
|
||||||
|
я: "ya",
|
||||||
|
};
|
||||||
|
const transliterated = String(value ?? "")
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[а-яё]/g, (char) => map[char] ?? "")
|
||||||
|
.replace(/[^a-z0-9]+/g, "-")
|
||||||
|
.replace(/^-+|-+$/g, "");
|
||||||
|
|
||||||
|
return transliterated || fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePathSegment(node, index) {
|
||||||
|
return slugify(node.slug || node.title || node.id, `page-${index + 1}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function publicAssetUrl(value) {
|
||||||
|
const url = String(value || "").trim();
|
||||||
|
if (!url) return "";
|
||||||
|
if (/^(https?:|data:|blob:|\/)/i.test(url)) return url;
|
||||||
|
if (url.startsWith("./")) return `/${url.slice(2)}`;
|
||||||
|
return `/${url.replace(/^\/+/, "")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeContentAssets(html) {
|
||||||
|
return String(html || "")
|
||||||
|
.replace(/\s(src|href)="\.\/assets\//g, ' $1="/assets/')
|
||||||
|
.replace(/\s(src|href)="assets\//g, ' $1="/assets/');
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeShortcodeAttr(value) {
|
||||||
|
return String(value ?? "")
|
||||||
|
.replaceAll(""", '"')
|
||||||
|
.replaceAll(""", '"')
|
||||||
|
.replaceAll("'", "'")
|
||||||
|
.replaceAll("'", "'")
|
||||||
|
.replaceAll("<", "<")
|
||||||
|
.replaceAll(">", ">")
|
||||||
|
.replaceAll("&", "&");
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseShortcodeAttrs(value) {
|
||||||
|
const attrs = {};
|
||||||
|
String(value || "").replace(
|
||||||
|
/([a-zA-Z][\w-]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"']+))/g,
|
||||||
|
(match, key, doubleQuoted, singleQuoted, bare) => {
|
||||||
|
attrs[key.toLowerCase()] = decodeShortcodeAttr(doubleQuoted ?? singleQuoted ?? bare ?? "");
|
||||||
|
return match;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return attrs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeKnowledgeButtonTarget(value) {
|
||||||
|
return String(value || "")
|
||||||
|
.trim()
|
||||||
|
.replace(/^#+/, "")
|
||||||
|
.replace(/^index\.html#/i, "")
|
||||||
|
.replace(/^\/?#/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeKnowledgeButtonLink(value) {
|
||||||
|
return (String(value ?? "").trim() || "/").replace(/^\/+(https?:\/\/)/i, "$1").replace(/^\/+((?:mailto|tel):)/i, "$1");
|
||||||
|
}
|
||||||
|
|
||||||
|
function allHomeBlocks() {
|
||||||
|
return (home.regions || []).flatMap((region) => region.blocks || []);
|
||||||
|
}
|
||||||
|
|
||||||
|
function landingBlockById(id) {
|
||||||
|
const blockId = String(id || "").trim();
|
||||||
|
if (!blockId) return null;
|
||||||
|
return allHomeBlocks().find((block) => block.id === blockId && block.enabled !== false) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function landingTargetForAttrs(attrs) {
|
||||||
|
const blockId = attrs.block || attrs["block-id"] || attrs.blockid || attrs.landingblock || attrs["landing-block"];
|
||||||
|
const blockTarget = normalizeKnowledgeButtonTarget(landingBlockById(blockId)?.anchor || "");
|
||||||
|
return blockTarget || normalizeKnowledgeButtonTarget(attrs.target || attrs.anchor || "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function knowledgeButtonHref(link, attrs) {
|
||||||
|
const anchor = landingTargetForAttrs(attrs);
|
||||||
|
const rawLink = normalizeKnowledgeButtonLink(link);
|
||||||
|
if (!anchor) return rawLink;
|
||||||
|
const base = rawLink.split("#")[0] || "/";
|
||||||
|
if (/^index\.html$/i.test(base)) return `/#${anchor}`;
|
||||||
|
return `${base}#${anchor}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderKnowledgeShortcodes(html) {
|
||||||
|
return String(html || "").replace(/\[button\b([^\]]*)\]/gi, (match, rawAttrs) => {
|
||||||
|
const attrs = parseShortcodeAttrs(rawAttrs);
|
||||||
|
const label = attrs.name || attrs.label || attrs.text || "Посмотреть";
|
||||||
|
const href = knowledgeButtonHref(attrs.link || attrs.href || "/", attrs);
|
||||||
|
return `<a class="knowledge-button" href="${escapeAttr(href)}">${escapeHtml(label)}</a>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function canonicalHome() {
|
||||||
|
const fallback = "https://nodedc.dctouch.ru/";
|
||||||
|
const value = String(home.seo?.canonicalUrl || fallback).trim() || fallback;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return new URL(value).href;
|
||||||
|
} catch {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function canonicalBase() {
|
||||||
|
const url = new URL(canonicalHome());
|
||||||
|
return `${url.origin}/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function knowledgeBasePath() {
|
||||||
|
return String(knowledge.basePath || "knowledge").replace(/^\/+|\/+$/g, "") || "knowledge";
|
||||||
|
}
|
||||||
|
|
||||||
|
function nodeChildren(node) {
|
||||||
|
return Array.isArray(node.children) ? node.children : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function nodeHasPage(node) {
|
||||||
|
return node?.enabled !== false && node.useContent !== false && stripHtml(renderKnowledgeShortcodes(node.bodyHtml || "")).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function walkNodes(nodes = knowledge.tree || [], parentSegments = [], parents = [], result = []) {
|
||||||
|
nodes.forEach((node, index) => {
|
||||||
|
if (node.enabled === false) return;
|
||||||
|
const segment = normalizePathSegment(node, index);
|
||||||
|
const segments = [...parentSegments, segment];
|
||||||
|
const record = {
|
||||||
|
node,
|
||||||
|
parents,
|
||||||
|
segment,
|
||||||
|
segments,
|
||||||
|
urlPath: `/${knowledgeBasePath()}/${segments.join("/")}/`,
|
||||||
|
};
|
||||||
|
result.push(record);
|
||||||
|
walkNodes(nodeChildren(node), segments, [...parents, record], result);
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pageRecords() {
|
||||||
|
return walkNodes().filter((record) => nodeHasPage(record.node));
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentAncestorIds(record) {
|
||||||
|
return new Set([record?.node?.id, ...(record?.parents || []).map((parent) => parent.node.id)].filter(Boolean));
|
||||||
|
}
|
||||||
|
|
||||||
|
function caretIcon(className = "caret astro-j63poqv6 astro-fr7meb3z", size = "1.25rem") {
|
||||||
|
return `<svg aria-hidden="true" class="${className}" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: ${size};"><path d="m14.83 11.29-4.24-4.24a1 1 0 1 0-1.42 1.41L12.71 12l-3.54 3.54a1 1 0 0 0 0 1.41 1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.24-4.24a1.002 1.002 0 0 0 0-1.42Z"/></svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchIcon() {
|
||||||
|
return '<svg aria-hidden="true" class="astro-2f6du2pg astro-fr7meb3z" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M21.71 20.29 18 16.61A9 9 0 1 0 16.61 18l3.68 3.68a.999.999 0 0 0 1.42 0 1 1 0 0 0 0-1.39ZM11 18a7 7 0 1 1 0-14 7 7 0 0 1 0 14Z"/></svg>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function anchorIcon(id, title) {
|
||||||
|
return `<a class="sl-anchor-link" href="#${escapeAttr(id)}"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentcolor" d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z"></path></svg></span><span class="sr-only" data-pagefind-ignore="">Раздел «${escapeHtml(title)}»</span></a>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function headingId(rawTitle, usedIds) {
|
||||||
|
const base = slugify(stripHtml(rawTitle), "section");
|
||||||
|
let id = base;
|
||||||
|
let index = 2;
|
||||||
|
while (usedIds.has(id)) {
|
||||||
|
id = `${base}-${index}`;
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
usedIds.add(id);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareArticleHtml(html) {
|
||||||
|
const usedIds = new Set();
|
||||||
|
const toc = [];
|
||||||
|
const normalized = renderKnowledgeShortcodes(normalizeContentAssets(html)).replace(/<h([23])([^>]*)>([\s\S]*?)<\/h\1>/gi, (match, level, attrs, inner) => {
|
||||||
|
const existingId = /\sid=(["'])(.*?)\1/i.exec(attrs);
|
||||||
|
const id = existingId?.[2] || headingId(inner, usedIds);
|
||||||
|
const title = stripHtml(inner);
|
||||||
|
if (existingId) usedIds.add(id);
|
||||||
|
toc.push({ level: Number(level), id, title });
|
||||||
|
const nextAttrs = existingId ? attrs : `${attrs} id="${escapeAttr(id)}"`;
|
||||||
|
return `<div class="sl-heading-wrapper level-h${level}"><h${level}${nextAttrs}>${inner}</h${level}>${anchorIcon(id, title)}</div>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { html: normalized, toc };
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNavRecord(record, currentIds, currentId, restoreCounter) {
|
||||||
|
const children = nodeChildren(record.node)
|
||||||
|
.map((child, index) => {
|
||||||
|
if (child.enabled === false) return "";
|
||||||
|
const childSegment = normalizePathSegment(child, index);
|
||||||
|
return renderNavRecord(
|
||||||
|
{
|
||||||
|
node: child,
|
||||||
|
parents: [...record.parents, record],
|
||||||
|
segment: childSegment,
|
||||||
|
segments: [...record.segments, childSegment],
|
||||||
|
urlPath: `/${knowledgeBasePath()}/${[...record.segments, childSegment].join("/")}/`,
|
||||||
|
},
|
||||||
|
currentIds,
|
||||||
|
currentId,
|
||||||
|
restoreCounter,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("");
|
||||||
|
const title = escapeHtml(record.node.title || record.node.id || "Материал");
|
||||||
|
const isCurrent = record.node.id === currentId;
|
||||||
|
const currentAttr = isCurrent ? ' aria-current="page"' : "";
|
||||||
|
|
||||||
|
if (!children) {
|
||||||
|
const largeClass = record.parents.length === 0 ? "large " : "";
|
||||||
|
const href = nodeHasPage(record.node) ? record.urlPath : "#";
|
||||||
|
return `<li class="astro-j63poqv6"><a href="${escapeAttr(href)}"${currentAttr} class="${largeClass}astro-j63poqv6"><span class="astro-j63poqv6">${title}</span></a></li>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const restoreIndex = restoreCounter.value;
|
||||||
|
restoreCounter.value += 1;
|
||||||
|
const open = currentIds.has(record.node.id) || record.parents.length === 0;
|
||||||
|
const label = nodeHasPage(record.node)
|
||||||
|
? `<a href="${escapeAttr(record.urlPath)}" class="knowledge-summary-link astro-j63poqv6"><span class="large astro-j63poqv6">${title}</span></a>`
|
||||||
|
: `<span class="large astro-j63poqv6">${title}</span>`;
|
||||||
|
|
||||||
|
return `<li class="astro-j63poqv6"><details ${open ? "open " : ""}class="astro-j63poqv6"><summary${currentAttr} class="astro-j63poqv6"><span class="group-label astro-j63poqv6">${label}</span>${caretIcon()}</summary><sl-sidebar-restore data-index="${restoreIndex}"></sl-sidebar-restore><ul class="astro-j63poqv6">${children}</ul></details></li>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSidebar(currentRecord) {
|
||||||
|
const currentIds = currentAncestorIds(currentRecord);
|
||||||
|
const currentId = currentRecord?.node?.id || "";
|
||||||
|
const restoreCounter = { value: 0 };
|
||||||
|
const baseActive = currentRecord ? "" : ' aria-current="page"';
|
||||||
|
const items = (knowledge.tree || [])
|
||||||
|
.map((node, index) => {
|
||||||
|
if (node.enabled === false) return "";
|
||||||
|
const segment = normalizePathSegment(node, index);
|
||||||
|
return renderNavRecord(
|
||||||
|
{
|
||||||
|
node,
|
||||||
|
parents: [],
|
||||||
|
segment,
|
||||||
|
segments: [segment],
|
||||||
|
urlPath: `/${knowledgeBasePath()}/${segment}/`,
|
||||||
|
},
|
||||||
|
currentIds,
|
||||||
|
currentId,
|
||||||
|
restoreCounter,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
return `<nav class="sidebar print:hidden astro-iyrf6vv4" aria-label="Основная навигация">
|
||||||
|
<starlight-menu-button class="print:hidden astro-yrbfzory">
|
||||||
|
<button aria-expanded="false" aria-label="Меню" aria-controls="starlight__sidebar" class="sl-flex md:sl-hidden astro-yrbfzory">${menuIcons()}</button>
|
||||||
|
</starlight-menu-button>
|
||||||
|
<div id="starlight__sidebar" class="sidebar-pane astro-iyrf6vv4">
|
||||||
|
<div class="sidebar-content sl-flex astro-iyrf6vv4">
|
||||||
|
<sl-sidebar-state-persist data-hash="nodedc-knowledge" class="astro-fouifdpt">
|
||||||
|
<ul class="top-level astro-j63poqv6">
|
||||||
|
<li class="astro-j63poqv6"><a href="/${knowledgeBasePath()}/"${baseActive} class="large astro-j63poqv6"><span class="astro-j63poqv6">${escapeHtml(knowledge.title || "База знаний")}</span></a></li>${items}
|
||||||
|
</ul>
|
||||||
|
</sl-sidebar-state-persist>
|
||||||
|
<div class="md:sl-hidden">
|
||||||
|
<div class="mobile-preferences sl-flex astro-22fp5ct5">
|
||||||
|
<div class="social-icons astro-22fp5ct5"></div>${renderThemeSelect()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function menuIcons() {
|
||||||
|
return `<svg aria-hidden="true" class="open-menu astro-yrbfzory astro-fr7meb3z" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M3 8h18a1 1 0 1 0 0-2H3a1 1 0 0 0 0 2Zm18 8H3a1 1 0 0 0 0 2h18a1 1 0 0 0 0-2Zm0-5H3a1 1 0 0 0 0 2h18a1 1 0 0 0 0-2Z"/></svg><svg aria-hidden="true" class="close-menu astro-yrbfzory astro-fr7meb3z" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="m13.41 12 6.3-6.29a1.004 1.004 0 1 0-1.42-1.42L12 10.59l-6.29-6.3a1.004 1.004 0 0 0-1.42 1.42l6.3 6.29-6.3 6.29a1 1 0 0 0 0 1.42.998.998 0 0 0 1.42 0l6.29-6.3 6.29 6.3a.999.999 0 0 0 1.42 0 1 1 0 0 0 0-1.42L13.41 12Z"/></svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTocItems(toc, mobile = false) {
|
||||||
|
const items = [{ level: 2, id: "_top", title: "Обзор" }, ...toc];
|
||||||
|
return items
|
||||||
|
.map((item) => {
|
||||||
|
const depth = Math.max(0, item.level - 2);
|
||||||
|
return `<li class="astro-uehw6aht" style="--depth: ${depth};"><a href="#${escapeAttr(item.id)}" class="astro-uehw6aht" style="--depth: ${depth};"><span class="astro-uehw6aht" style="--depth: ${depth};">${escapeHtml(item.title)}</span></a></li>`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRightSidebar(toc) {
|
||||||
|
const items = renderTocItems(toc);
|
||||||
|
const mobileItems = renderTocItems(toc, true);
|
||||||
|
return `<aside class="right-sidebar-container print:hidden astro-6ptv7odt">
|
||||||
|
<div class="right-sidebar astro-6ptv7odt">
|
||||||
|
<script type="module" src="/assets/vendor/ssscript-docs/_astro/MobileTableOfContents.astro_astro_type_script_index_0_lang.hwBsy0Mo.js"></script>
|
||||||
|
<script type="module" src="/assets/vendor/ssscript-docs/_astro/TableOfContents.astro_astro_type_script_index_0_lang.FuRcXuRY.js"></script>
|
||||||
|
<div class="lg:sl-hidden astro-auw6kndz">
|
||||||
|
<mobile-starlight-toc data-min-h="2" data-max-h="3" class="astro-2zbszx3p">
|
||||||
|
<nav aria-labelledby="starlight__on-this-page--mobile" class="astro-2zbszx3p">
|
||||||
|
<details id="starlight__mobile-toc" class="astro-2zbszx3p">
|
||||||
|
<summary id="starlight__on-this-page--mobile" class="sl-flex astro-2zbszx3p"><span class="toggle sl-flex astro-2zbszx3p">На этой странице${caretIcon("caret astro-2zbszx3p astro-fr7meb3z", "1rem")}</span><span class="display-current astro-2zbszx3p"></span></summary>
|
||||||
|
<div class="dropdown astro-2zbszx3p"><ul class="isMobile astro-uehw6aht" style="--depth: 0;">${mobileItems}</ul></div>
|
||||||
|
</details>
|
||||||
|
</nav>
|
||||||
|
</mobile-starlight-toc>
|
||||||
|
</div>
|
||||||
|
<div class="right-sidebar-panel sl-hidden lg:sl-block astro-auw6kndz">
|
||||||
|
<div class="sl-container astro-auw6kndz">
|
||||||
|
<starlight-toc data-min-h="2" data-max-h="3">
|
||||||
|
<nav aria-labelledby="starlight__on-this-page">
|
||||||
|
<h2 id="starlight__on-this-page">На этой странице</h2>
|
||||||
|
<ul class="astro-uehw6aht" style="--depth: 0;">${items}</ul>
|
||||||
|
</nav>
|
||||||
|
</starlight-toc>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderThemeIconsTemplate() {
|
||||||
|
return `<template id="theme-icons"><svg aria-hidden="true" class="light astro-fr7meb3z" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M5 12a1 1 0 0 0-1-1H3a1 1 0 0 0 0 2h1a1 1 0 0 0 1-1Zm.64 5-.71.71a1 1 0 0 0 0 1.41 1 1 0 0 0 1.41 0l.71-.71A1 1 0 0 0 5.64 17ZM12 5a1 1 0 0 0 1-1V3a1 1 0 0 0-2 0v1a1 1 0 0 0 1 1Zm5.66 2.34a1 1 0 0 0 .7-.29l.71-.71a1 1 0 1 0-1.41-1.41l-.66.71a1 1 0 0 0 0 1.41 1 1 0 0 0 .66.29Zm-12-.29a1 1 0 0 0 1.41 0 1 1 0 0 0 0-1.41l-.71-.71a1.004 1.004 0 1 0-1.43 1.41l.73.71ZM21 11h-1a1 1 0 0 0 0 2h1a1 1 0 0 0 0-2Zm-2.64 6A1 1 0 0 0 17 18.36l.71.71a1 1 0 0 0 1.41 0 1 1 0 0 0 0-1.41l-.76-.66ZM12 6.5a5.5 5.5 0 1 0 5.5 5.5A5.51 5.51 0 0 0 12 6.5Zm0 9a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7Zm0 3.5a1 1 0 0 0-1 1v1a1 1 0 0 0 2 0v-1a1 1 0 0 0-1-1Z"/></svg><svg aria-hidden="true" class="dark astro-fr7meb3z" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M21.64 13a1 1 0 0 0-1.05-.14 8.049 8.049 0 0 1-3.37.73 8.15 8.15 0 0 1-8.14-8.1 8.59 8.59 0 0 1 .25-2A1 1 0 0 0 8 2.36a10.14 10.14 0 1 0 14 11.69 1 1 0 0 0-.36-1.05Zm-9.5 6.69A8.14 8.14 0 0 1 7.08 5.22v.27a10.15 10.15 0 0 0 10.14 10.14 9.784 9.784 0 0 0 2.1-.22 8.11 8.11 0 0 1-7.18 4.32v-.04Z"/></svg><svg aria-hidden="true" class="auto astro-fr7meb3z" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M21 14h-1V7a3 3 0 0 0-3-3H7a3 3 0 0 0-3 3v7H3a1 1 0 0 0-1 1v2a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3v-2a1 1 0 0 0-1-1ZM6 7a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v7H6V7Zm14 10a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-1h16v1Z"/></svg></template>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderThemeSelect() {
|
||||||
|
return `<starlight-theme-select><label style="--sl-select-width: 6.25em" class="astro-47nlhtql"><span class="sr-only astro-47nlhtql">Выбор темы</span><svg aria-hidden="true" class="icon label-icon astro-47nlhtql astro-fr7meb3z" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M21 14h-1V7a3 3 0 0 0-3-3H7a3 3 0 0 0-3 3v7H3a1 1 0 0 0-1 1v2a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3v-2a1 1 0 0 0-1-1ZM6 7a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v7H6V7Zm14 10a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-1h16v1Z"/></svg><select autocomplete="off" class="astro-47nlhtql"><option value="dark" class="astro-47nlhtql">Dark</option><option value="light" class="astro-47nlhtql">Light</option><option value="auto" class="astro-47nlhtql">Auto</option></select><svg aria-hidden="true" class="icon caret astro-47nlhtql astro-fr7meb3z" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M17 9.17a1 1 0 0 0-1.41 0L12 12.71 8.46 9.17a1 1 0 1 0-1.41 1.42l4.24 4.24a1.002 1.002 0 0 0 1.42 0L17 10.59a1.002 1.002 0 0 0 0-1.42Z"/></svg></label></starlight-theme-select>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function themeProviderScript() {
|
||||||
|
const defaultTheme = escapeAttr(knowledge.settings?.themeDefault || "dark");
|
||||||
|
return `<script>
|
||||||
|
window.StarlightThemeProvider = (() => {
|
||||||
|
const storedTheme = typeof localStorage !== "undefined" && localStorage.getItem("starlight-theme");
|
||||||
|
const theme = storedTheme || "${defaultTheme}";
|
||||||
|
document.documentElement.dataset.theme = theme === "auto" ? (window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark") : theme;
|
||||||
|
return {
|
||||||
|
updatePickers(theme = storedTheme || "${defaultTheme}") {
|
||||||
|
document.querySelectorAll("starlight-theme-select").forEach((picker) => {
|
||||||
|
const select = picker.querySelector("select");
|
||||||
|
if (select) select.value = theme;
|
||||||
|
const tmpl = document.querySelector("#theme-icons");
|
||||||
|
const newIcon = tmpl && tmpl.content.querySelector("." + theme);
|
||||||
|
if (newIcon) {
|
||||||
|
const oldIcon = picker.querySelector("svg.label-icon");
|
||||||
|
if (oldIcon) oldIcon.replaceChildren(...newIcon.cloneNode(true).childNodes);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
</script>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function interactionScript() {
|
||||||
|
return `<script type="module">
|
||||||
|
const themeKey = "starlight-theme";
|
||||||
|
const defaultTheme = "${escapeAttr(knowledge.settings?.themeDefault || "dark")}";
|
||||||
|
const normalizeTheme = (value) => value === "auto" || value === "dark" || value === "light" ? value : defaultTheme;
|
||||||
|
const readTheme = () => normalizeTheme(typeof localStorage !== "undefined" && localStorage.getItem(themeKey));
|
||||||
|
const systemTheme = () => matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
|
||||||
|
function storeTheme(theme) {
|
||||||
|
if (typeof localStorage !== "undefined") localStorage.setItem(themeKey, theme);
|
||||||
|
}
|
||||||
|
function applyTheme(theme) {
|
||||||
|
StarlightThemeProvider.updatePickers(theme);
|
||||||
|
document.documentElement.dataset.theme = theme === "auto" ? systemTheme() : theme;
|
||||||
|
storeTheme(theme);
|
||||||
|
}
|
||||||
|
matchMedia("(prefers-color-scheme: light)").addEventListener("change", () => {
|
||||||
|
if (readTheme() === "auto") applyTheme("auto");
|
||||||
|
});
|
||||||
|
class StarlightThemeSelect extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
applyTheme(readTheme());
|
||||||
|
this.querySelector("select")?.addEventListener("change", (event) => {
|
||||||
|
if (event.currentTarget instanceof HTMLSelectElement) applyTheme(normalizeTheme(event.currentTarget.value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!customElements.get("starlight-theme-select")) customElements.define("starlight-theme-select", StarlightThemeSelect);
|
||||||
|
|
||||||
|
class StarlightMenuButton extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.btn = this.querySelector("button");
|
||||||
|
this.btn?.addEventListener("click", () => this.toggleExpanded());
|
||||||
|
const nav = this.closest("nav");
|
||||||
|
nav?.addEventListener("keyup", (event) => this.closeOnEscape(event));
|
||||||
|
}
|
||||||
|
setExpanded(expanded) {
|
||||||
|
this.setAttribute("aria-expanded", String(expanded));
|
||||||
|
this.btn?.setAttribute("aria-expanded", String(expanded));
|
||||||
|
document.body.toggleAttribute("data-mobile-menu-expanded", expanded);
|
||||||
|
}
|
||||||
|
toggleExpanded() {
|
||||||
|
this.setExpanded(this.getAttribute("aria-expanded") !== "true");
|
||||||
|
}
|
||||||
|
closeOnEscape(event) {
|
||||||
|
if (event.code === "Escape") {
|
||||||
|
this.setExpanded(false);
|
||||||
|
this.btn?.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!customElements.get("starlight-menu-button")) customElements.define("starlight-menu-button", StarlightMenuButton);
|
||||||
|
|
||||||
|
const sidebar = document.getElementById("starlight__sidebar");
|
||||||
|
const persist = sidebar?.querySelector("sl-sidebar-state-persist");
|
||||||
|
const storageKey = "sl-sidebar-state:nodedc-knowledge";
|
||||||
|
const readState = () => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(sessionStorage.getItem(storageKey) || "{}");
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const writeState = () => {
|
||||||
|
if (!sidebar || !persist) return;
|
||||||
|
const details = [...persist.querySelectorAll("details")];
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem(storageKey, JSON.stringify({ open: details.map((item) => item.open), scroll: sidebar.scrollTop }));
|
||||||
|
} catch {}
|
||||||
|
};
|
||||||
|
if (sidebar && persist && matchMedia("(min-width: 50em)").matches) {
|
||||||
|
const state = readState();
|
||||||
|
if (Array.isArray(state.open)) {
|
||||||
|
[...persist.querySelectorAll("details")].forEach((details, index) => {
|
||||||
|
if (typeof state.open[index] === "boolean") details.open = state.open[index];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (typeof state.scroll === "number") sidebar.scrollTop = state.scroll;
|
||||||
|
persist.addEventListener("toggle", writeState, true);
|
||||||
|
addEventListener("pagehide", writeState);
|
||||||
|
}
|
||||||
|
</script>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHeader(searchPlaceholder) {
|
||||||
|
const logoSrc = publicAssetUrl(home.staticElements?.content?.navigation?.logoSrc || home.staticElements?.content?.assets?.appIconSrc || "");
|
||||||
|
const logoAlt = home.staticElements?.content?.navigation?.logoAlt || "NODE.DC";
|
||||||
|
const brand = knowledge.settings?.navTitle || knowledge.title || "База знаний";
|
||||||
|
const headerHref = knowledge.settings?.homeHref || "/";
|
||||||
|
|
||||||
|
return `<header class="header astro-iyrf6vv4">
|
||||||
|
<div class="max-md:flex gap-(--sl-nav-gap) justify-between items-center h-full header-grid astro-3ef6ksr2">
|
||||||
|
<div class="overflow-clip p-1 -m-1 min-w-0 sl-flex title-wrapper astro-3ef6ksr2">
|
||||||
|
<a href="${escapeAttr(headerHref)}" class="flex items-center gap-(--sl-nav-gap) font-semibold no-underline whitespace-nowrap min-w-0 site-title astro-idrpryed">
|
||||||
|
<span class="site-title-logo inline-flex items-center shrink-0 astro-idrpryed" aria-hidden="true">${logoSrc ? `<img src="${escapeAttr(logoSrc)}" alt="${escapeAttr(logoAlt)}">` : ""}</span>
|
||||||
|
<span class="overflow-hidden astro-idrpryed" translate="no">${escapeHtml(brand)}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="sl-flex print:hidden search-wrapper justify-center astro-3ef6ksr2" data-search>
|
||||||
|
<site-search class="astro-3ef6ksr2 astro-2f6du2pg" data-translations="{"placeholder":"${escapeAttr(searchPlaceholder)}"}">
|
||||||
|
<button data-open-modal aria-label="${escapeAttr(searchPlaceholder)}" aria-keyshortcuts="Meta+K" class="astro-2f6du2pg" type="button">${searchIcon()}<span class="sl-hidden md:sl-block astro-2f6du2pg" aria-hidden="true">${escapeHtml(searchPlaceholder)}</span><kbd class="sl-hidden md:sl-flex astro-2f6du2pg"><kbd class="astro-2f6du2pg">⌘</kbd><kbd class="astro-2f6du2pg">K</kbd></kbd></button>
|
||||||
|
</site-search>
|
||||||
|
<div class="knowledge-search-popover" data-search-popover hidden>
|
||||||
|
<input class="knowledge-search-input" type="search" placeholder="${escapeAttr(searchPlaceholder)}" autocomplete="off" data-search-input>
|
||||||
|
<div class="knowledge-search-results" data-search-results></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sl-hidden md:sl-flex print:hidden flex gap-4 items-center right-group astro-3ef6ksr2">
|
||||||
|
<div class="sl-flex gap-4 items-center social-icons astro-3ef6ksr2"></div>
|
||||||
|
<a href="#" class="inline-flex items-center gap-2 font-medium no-underline whitespace-nowrap transition-[color,filter] duration-150 download-cta knowledge-download-placeholder py-[0.3rem] px-[0.5rem] pl-[0.4rem] text-sm rounded-[8px] astro-sle7k2hz" aria-hidden="true" tabindex="-1"><span class="inline-flex items-center justify-center flex-shrink-0 w-6 h-6 astro-sle7k2hz"></span><span class="astro-sle7k2hz">Download</span></a>
|
||||||
|
${renderThemeSelect()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pageTitle(record = null) {
|
||||||
|
if (!record) return knowledge.title || "База знаний";
|
||||||
|
return record.node.seo?.title || record.node.title || "Материал";
|
||||||
|
}
|
||||||
|
|
||||||
|
function visibleTitle(record = null) {
|
||||||
|
if (!record) return knowledge.title || "База знаний";
|
||||||
|
return record.node.title || "Материал";
|
||||||
|
}
|
||||||
|
|
||||||
|
function pageDescription(record = null) {
|
||||||
|
if (!record) return knowledge.settings?.description || "";
|
||||||
|
return record.node.seo?.description || stripHtml(renderKnowledgeShortcodes(record.node.bodyHtml)).slice(0, 180);
|
||||||
|
}
|
||||||
|
|
||||||
|
function canonicalForPath(path) {
|
||||||
|
return new URL(path.replace(/^\//, ""), canonicalBase()).href;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderIndexCards(records) {
|
||||||
|
const topPages = records.filter((record) => record.parents.length <= 1).slice(0, 12);
|
||||||
|
if (!topPages.length) return "";
|
||||||
|
return `<h2>Материалы</h2><ul class="knowledge-card-list">${topPages
|
||||||
|
.map(
|
||||||
|
(record) =>
|
||||||
|
`<li><a href="${escapeAttr(record.urlPath)}">${escapeHtml(record.node.title)}</a> — ${escapeHtml(pageDescription(record))}</li>`,
|
||||||
|
)
|
||||||
|
.join("")}</ul>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderContent(record, bodyHtml) {
|
||||||
|
const title = visibleTitle(record);
|
||||||
|
return `<div class="main-pane astro-6ptv7odt">
|
||||||
|
<main data-pagefind-body class="astro-ept3ui5y" lang="ru" dir="ltr">
|
||||||
|
<div class="content-panel astro-hxjpthjh">
|
||||||
|
<div class="sl-container astro-hxjpthjh">
|
||||||
|
<h1 id="_top" data-page-title class="astro-gldynaio">${escapeHtml(title)}</h1>
|
||||||
|
<div class="sl-markdown-content">${bodyHtml}</div>
|
||||||
|
<footer class="sl-flex astro-vcuyfr7s"><div class="meta sl-flex astro-vcuyfr7s"></div></footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPage({ record = null, bodyHtml, toc = [], searchRecords }) {
|
||||||
|
const title = pageTitle(record);
|
||||||
|
const description = pageDescription(record);
|
||||||
|
const path = record?.urlPath || `/${knowledgeBasePath()}/`;
|
||||||
|
const canonical = canonicalForPath(path);
|
||||||
|
const searchPlaceholder = knowledge.settings?.searchPlaceholder || "Поиск";
|
||||||
|
|
||||||
|
return `<!doctype html>
|
||||||
|
<html lang="ru" dir="ltr" data-theme="${escapeAttr(knowledge.settings?.themeDefault || "dark")}" data-has-toc data-has-sidebar class="astro-ept3ui5y">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>${escapeHtml(title)}</title>
|
||||||
|
<link rel="shortcut icon" href="/assets/vendor/ssscript-docs/favicon.svg" type="image/svg+xml">
|
||||||
|
<meta name="generator" content="Astro v5.18.0">
|
||||||
|
<meta name="generator" content="Starlight v0.37.7">
|
||||||
|
<meta name="description" content="${escapeAttr(description)}">
|
||||||
|
<meta name="robots" content="index,follow,max-image-preview:large">
|
||||||
|
<meta property="og:title" content="${escapeAttr(title)}">
|
||||||
|
<meta property="og:type" content="article">
|
||||||
|
<meta property="og:locale" content="ru">
|
||||||
|
<meta property="og:description" content="${escapeAttr(description)}">
|
||||||
|
<meta property="og:site_name" content="NODE.DC">
|
||||||
|
<meta property="og:url" content="${escapeAttr(canonical)}">
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<link rel="canonical" href="${escapeAttr(canonical)}">
|
||||||
|
${themeProviderScript()}
|
||||||
|
${renderThemeIconsTemplate()}
|
||||||
|
<link rel="stylesheet" href="/assets/vendor/ssscript-docs/_astro/print.DNXP8c50.css" media="print">
|
||||||
|
<link rel="stylesheet" href="/assets/vendor/ssscript-docs/_astro/index.Bh6uiXgK.css">
|
||||||
|
<link rel="stylesheet" href="/assets/custom/css/knowledge.css">
|
||||||
|
<script type="module" src="/assets/vendor/ssscript-docs/_astro/page.B1D-nYk3.js"></script>
|
||||||
|
<script type="application/ld+json">${escapeJson({
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": record ? "Article" : "CollectionPage",
|
||||||
|
headline: title,
|
||||||
|
description,
|
||||||
|
url: canonical,
|
||||||
|
author: {
|
||||||
|
"@type": "Organization",
|
||||||
|
name: home.seo?.organizationName || "DCTOUCH",
|
||||||
|
},
|
||||||
|
publisher: {
|
||||||
|
"@type": "Organization",
|
||||||
|
name: home.seo?.organizationName || "DCTOUCH",
|
||||||
|
},
|
||||||
|
})}</script>
|
||||||
|
</head>
|
||||||
|
<body class="astro-ept3ui5y">
|
||||||
|
<a href="#_top" class="astro-yr4ykvqi">К содержанию</a>
|
||||||
|
<div class="page sl-flex astro-iyrf6vv4">
|
||||||
|
${renderHeader(searchPlaceholder)}
|
||||||
|
${renderSidebar(record)}
|
||||||
|
<div class="main-frame astro-iyrf6vv4">
|
||||||
|
<div class="lg:sl-flex astro-6ptv7odt">
|
||||||
|
${renderRightSidebar(toc)}
|
||||||
|
${renderContent(record, bodyHtml)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script type="application/json" id="knowledge-search-index">${escapeJson(searchRecords)}</script>
|
||||||
|
${interactionScript()}
|
||||||
|
${searchScript()}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchScript() {
|
||||||
|
return `<script>
|
||||||
|
(() => {
|
||||||
|
const root = document.querySelector("[data-search]");
|
||||||
|
const button = root?.querySelector("[data-open-modal]");
|
||||||
|
const popover = root?.querySelector("[data-search-popover]");
|
||||||
|
const input = root?.querySelector("[data-search-input]");
|
||||||
|
const results = root?.querySelector("[data-search-results]");
|
||||||
|
const index = JSON.parse(document.getElementById("knowledge-search-index")?.textContent || "[]");
|
||||||
|
const escapeText = (value) => String(value ?? "")
|
||||||
|
.replaceAll("&", "&")
|
||||||
|
.replaceAll("<", "<")
|
||||||
|
.replaceAll(">", ">")
|
||||||
|
.replaceAll('"', """)
|
||||||
|
.replaceAll("'", "'");
|
||||||
|
const render = () => {
|
||||||
|
const query = input.value.trim().toLowerCase();
|
||||||
|
const matches = query
|
||||||
|
? index.filter((item) => [item.title, item.description, item.text].join(" ").toLowerCase().includes(query)).slice(0, 8)
|
||||||
|
: [];
|
||||||
|
results.innerHTML = !query
|
||||||
|
? '<div class="knowledge-search-result"><strong>Введите запрос</strong><span>Поиск идет по опубликованным материалам базы знаний.</span></div>'
|
||||||
|
: matches.length
|
||||||
|
? matches.map((item) => '<a class="knowledge-search-result" href="' + escapeText(item.url) + '"><strong>' + escapeText(item.title) + '</strong><span>' + escapeText(item.description) + '</span></a>').join("")
|
||||||
|
: '<div class="knowledge-search-result"><strong>Ничего не найдено</strong><span>Попробуйте другой запрос.</span></div>';
|
||||||
|
};
|
||||||
|
const open = () => {
|
||||||
|
if (!popover || !input) return;
|
||||||
|
popover.hidden = false;
|
||||||
|
render();
|
||||||
|
window.setTimeout(() => input.focus(), 0);
|
||||||
|
};
|
||||||
|
const close = () => {
|
||||||
|
if (popover) popover.hidden = true;
|
||||||
|
};
|
||||||
|
button?.addEventListener("click", () => {
|
||||||
|
if (popover?.hidden) open();
|
||||||
|
else close();
|
||||||
|
});
|
||||||
|
input?.addEventListener("input", render);
|
||||||
|
document.addEventListener("keydown", (event) => {
|
||||||
|
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
|
||||||
|
event.preventDefault();
|
||||||
|
open();
|
||||||
|
}
|
||||||
|
if (event.key === "Escape") close();
|
||||||
|
});
|
||||||
|
document.addEventListener("click", (event) => {
|
||||||
|
if (root && !root.contains(event.target)) close();
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writePage(urlPath, html) {
|
||||||
|
const outputDir = join(repoRoot, urlPath.replace(/^\/+|\/+$/g, ""));
|
||||||
|
await mkdir(outputDir, { recursive: true });
|
||||||
|
await writeFile(join(outputDir, "index.html"), html);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeSeoFiles(records) {
|
||||||
|
const sitemapUrls = [
|
||||||
|
{ loc: canonicalHome(), priority: "1.0" },
|
||||||
|
{ loc: canonicalForPath(`/${knowledgeBasePath()}/`), priority: "0.8" },
|
||||||
|
...records.map((record) => ({ loc: canonicalForPath(record.urlPath), priority: record.parents.length ? "0.6" : "0.7" })),
|
||||||
|
];
|
||||||
|
const sitemapUrl = new URL("sitemap.xml", canonicalHome()).href;
|
||||||
|
const robots = ["User-agent: *", "Allow: /", `Sitemap: ${sitemapUrl}`, ""].join("\n");
|
||||||
|
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
${sitemapUrls
|
||||||
|
.map(
|
||||||
|
(item) => ` <url>
|
||||||
|
<loc>${escapeXml(item.loc)}</loc>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>${item.priority}</priority>
|
||||||
|
</url>`,
|
||||||
|
)
|
||||||
|
.join("\n")}
|
||||||
|
</urlset>
|
||||||
|
`;
|
||||||
|
await writeFile(robotsPath, robots);
|
||||||
|
await writeFile(sitemapPath, sitemap);
|
||||||
|
}
|
||||||
|
|
||||||
|
const records = pageRecords();
|
||||||
|
const searchRecords = records.map((record) => ({
|
||||||
|
title: record.node.title || "",
|
||||||
|
description: pageDescription(record),
|
||||||
|
url: record.urlPath,
|
||||||
|
text: stripHtml(renderKnowledgeShortcodes(record.node.bodyHtml || "")),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const indexIntro = normalizeContentAssets(
|
||||||
|
knowledge.introHtml ||
|
||||||
|
`<p>${escapeHtml(knowledge.settings?.description || "Материалы NODE.DC для пользователей и поисковых систем.")}</p>`,
|
||||||
|
);
|
||||||
|
const indexPrepared = prepareArticleHtml(`${indexIntro}${renderIndexCards(records)}`);
|
||||||
|
await writePage(`/${knowledgeBasePath()}/`, renderPage({ bodyHtml: indexPrepared.html, toc: indexPrepared.toc, searchRecords }));
|
||||||
|
|
||||||
|
for (const record of records) {
|
||||||
|
const prepared = prepareArticleHtml(record.node.bodyHtml || "");
|
||||||
|
await writePage(record.urlPath, renderPage({ record, bodyHtml: prepared.html, toc: prepared.toc, searchRecords }));
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeSeoFiles(records);
|
||||||
|
console.log(`Rendered ${records.length + 1} knowledge pages -> /${knowledgeBasePath()}/`);
|
||||||
Loading…
Reference in New Issue