/* global React, ReactDOM, DesignCanvas, DCSection, DCArtboard, DCPostIt */
const { useState } = React;
// ---- white-label brand (M1) -----------------------------------------------
// The portal is baked to static HTML with window.__BRAND injected into
// at build time from brand.config.json (see scripts/brand-client.mjs). Every
// brand-tunable literal below reads from it, falling back to the current EdgeLab
// Space values so the raw prototype (opened without an injected __BRAND) still
// renders standalone. Swapping brand.config.json white-labels the whole UI.
const BRAND = (typeof window !== "undefined" && window.__BRAND) || {};
const BRAND_NAME = BRAND.brandName || "EdgeLab Space";
// Non-breaking-space variants so a multi-word brand name never wraps mid-name —
// these reproduce the original `EdgeLab Space` / `EDGELAB SPACE`
// literals (U+00A0 serializes back to in the prerendered HTML).
const BRAND_NB = BRAND_NAME.replace(/ /g, "\u00A0");
const BRAND_UPPER = BRAND_NAME.toUpperCase();
const BRAND_UPPER_NB = BRAND_UPPER.replace(/ /g, "\u00A0");
const FUNNEL = BRAND.funnel || {};
// mode:"none" removes every community CTA / promo banner from the UI.
const FUNNEL_ON = (FUNNEL.mode || "community") !== "none";
const CTA_TEXT = FUNNEL.ctaText || "Вступить в сообщество";
const FUNNEL_URL = (FUNNEL.targetUrl || "https://edgelab.space").replace(/\/+$/, "");
const FUNNEL_OFFER = FUNNEL.offerText || "Закрытое AI-комьюнити: библиотека скиллов, живой чат, эфиры по средам.";
// Home-hero niche texts (white-label M1 step 5) — the niche layer of the home
// page reads from config; EdgeLab literals are the fallbacks.
const HOME = BRAND.home || {};
const HOME_EYEBROW = HOME.eyebrow || "AI ПОРТАЛ";
const HOME_TITLE = HOME.title || "Статьи и гайды по трендовым AI решениям";
const HOME_DESC = HOME.description || "Разбираю на практике Claude Code, дизайн с AI, базы знаний и автоматизацию – живым языком, без сухих инструкций.";
const HOME_CTA_TEXT = HOME.ctaText || "Начать внедрение";
const HOME_CTA_HREF = HOME.ctaHref || "/guides/kak-vnedrit-ai-agentov/";
const HOME_HERO_IMG = HOME.heroImage || "/covers/_home-hero-dashi.webp";
const AUTHOR = BRAND.author || {};
const AUTHOR_NAME = AUTHOR.name || "Даши Ешиев";
const LEGAL_NAME = AUTHOR.legal || "EdgeLab Space";
const LEGAL_ENTITY = AUTHOR.legalEntity || "ИП ЕШИЕВ ЦЫРЕН-ДАШИ БУЛАТОВИЧ · ИНН 032315681243";
// Community-landing apex host — the click-time UTM forwarder rewrites only links
// that point here. Derived from the funnel target so white-labels work too.
const FUNNEL_HOST = (() => {
try { return new URL(FUNNEL_URL).hostname.replace(/^www\./, ""); } catch (_) { return "edgelab.space"; }
})();
// Deep-link attribution: Telegram ignores utm_* on t.me links — only ?start=
// reaches the bot. Encode source+placement as journal_. The click-time handler
// (wireStartAttribution) appends the article slug on /guides// pages → per-article.
const startLink = (place) => `${FUNNEL_URL}?start=journal_${place}`;
// Per-article attribution (2026-07-27): a bot CTA carries a static ?start=journal_
// (place = hdr/ftr/ban/abt). When the click happens on a /guides// page, append the
// slug → ?start=journal__, so the bot can log WHICH article converted. Telegram
// passes the whole start payload to /start; utm_* on t.me links is ignored, so we drop it.
// Click-time rewrite is prerender/hydration-safe (mutates href on click, no SSR markup change).
// No-JS clicks keep the place-level code. Payload stays ASCII [A-Za-z0-9_-], < 64 chars.
function wireStartAttribution() {
document.addEventListener("click", (e) => {
const a = e.target && e.target.closest && e.target.closest("a[href]");
if (!a) return;
let u;
try { u = new URL(a.href, location.origin); } catch (_) { return; }
if (u.hostname.replace(/^www\./, "") !== FUNNEL_HOST) return; // only the bot funnel host
const start = u.searchParams.get("start");
const m = start && start.match(/^journal_([a-z]+)$/); // only enrich bare journal_ codes
if (!m) return;
const path = location.pathname.replace(/\/+$/, "");
const gm = path.match(/\/guides\/([^/]+)/);
const page = gm
? gm[1]
: (path === "" || /\/index\.html$/.test(path) ? "home" : (path.split("/").pop() || "home").replace(/\.html$/, ""));
u.searchParams.set("start", `journal_${m[1]}_${page}`);
a.href = u.toString();
}, true);
}
try { if (typeof window !== "undefined" && window.document) { wireStartAttribution(); } } catch (_) {}
// M-theme: persist the chosen theme across pages. Default light; dark sticks.
// getStoredTheme seeds React state from localStorage; applyTheme mirrors the
// choice onto (so html/body bg is dark too — see CSS)
// + saves it; flipTheme toggles. An inline script applies the html class
// BEFORE paint to avoid a light flash (FOUC) on stored-dark navigations.
function getStoredTheme(fallback) {
try {
const v = window.localStorage.getItem("gfTheme");
if (v === "dark" || v === "light") return v;
} catch (e) { /* private mode / no storage */ }
// нет явного выбора → следуем дню/ночи устройства (тёмный режим телефона обычно
// включается по расписанию вечером) — авто-выбор темы по времени суток
try {
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) return "dark";
} catch (e) { /* noop */ }
return fallback || "light";
}
function applyTheme(t) {
try {
const d = document.documentElement;
if (t === "dark") d.classList.add("theme-dark"); else d.classList.remove("theme-dark");
window.localStorage.setItem("gfTheme", t);
} catch (e) { /* noop */ }
}
function flipTheme(setTheme) {
// Source of truth = the flag (drives html/body bg in CSS).
// applyTheme is a side effect (DOM class + localStorage) — it MUST run outside the
// setState updater. Running it inside made React's double-invoke (StrictMode) toggle
// the html class twice → it desynced from React state → dark content over a light
// html/body = white corner on overscroll after dark→light→dark. Flip off the DOM.
try {
const cur = document.documentElement.classList.contains("theme-dark") ? "dark" : "light";
const n = cur === "dark" ? "light" : "dark";
applyTheme(n);
setTheme(n);
} catch (e) {
setTheme((t) => (t === "dark" ? "light" : "dark"));
}
}
/* ---------- logo + header ---------- */
function CopyIcon() {
return (
);
}
function MoonIcon() {
return (
);
}
function SunIcon() {
return (
);
}
function ThemeToggle({ night, onClick }) {
return (
);
}
function Logo() {
// clicking the logo (header/footer/drawer) goes to the home page. The two-part
// wordmark is derived from the brand name (first word + the rest), so a
// white-label brand renders its own name; EdgeLab Space → «edgelab ▪ space».
const parts = BRAND_NAME.split(/\s+/);
const a = (parts[0] || BRAND_NAME).toLowerCase();
const b = parts.slice(1).join(" ").toLowerCase();
return (
{a}{b && }{b && {b}}
);
}
// Author byline under the title (guides + articles), Substack-style author header.
// `meta` = optional read-time/date shown after the author («Даши Ешиев · 15 мин чтения»).
function Byline({ meta }) {
return (
{TG_NAME}{AUTHOR_NAME}{meta ? " · " + meta : ""}
);
}
const TG = (BRAND.telegram && BRAND.telegram.channelUrl) || "https://t.me/dashi_agent";
const TG_NAME = (BRAND.telegram && BRAND.telegram.channelName) || "Записки AI-энтузиаста";
// funnel docs + landing anchors — готовые ссылки с edgelab.space (footer)
const DOC_PRIVACY = (FUNNEL.docs && FUNNEL.docs.privacy) || "https://drive.google.com/file/d/1hUEZbJBCdTQcpBqy_OAd8EupZwY9Xpx1/view";
const DOC_OFFER = (FUNNEL.docs && FUNNEL.docs.offer) || "https://drive.google.com/file/d/1i6XeY6SMcuA4uYYxBVpxEPN_EI_AeIH6/view";
const ELS_STREAMS = `${FUNNEL_URL}/#streams`;
const ELS_COMMUNITY = `${FUNNEL_URL}/#community`;
function TelegramButton() {
return (
Телеграм-канал
);
}
/* ---------- portal navigation ---------- */
// Absolute clean URLs — work from any page depth / trailing-slash / device-served
// path. No .html, no relative ../ juggling (that broke styling on clean URLs).
const PAGES = { home: "/", library: "/library/", articles: "/#articles", claude: "/claude-code/", hermes: "/hermes/", codex: "/codex/", about: "/about/", guide: "/guide/" };
const GF_BASE = "";
const GUIDE_HREF = (slug) => `/guides/${slug}/`;
const P = (key) => PAGES[key];
const NAV = [["home", "Главная"], ["library", "Гайды"], ["articles", "Статьи"], ["about", "О проекте"]];
function NavLinks({ cur }) {
return (
);
}
function JoinBtn() {
if (!FUNNEL_ON) return null;
return ({CTA_TEXT} →);
}
function MobileMenuPanel({ cur, open, onClose }) {
return (
ДОСТУП К ЗАБЛОКИРОВАННОМУ· YouTube· WhatsApp· инструкции 2026
)}
);
}
/* ---------- banners ---------- */
// VOX: EdgeLab-баннеры про «AI-комьюнити/эфиры» убраны. Мягкий заход в продукт —
// только внутри статей (в тексте) + одна кнопка в шапке.
function BannerStrip() { return null; }
function BannerCard() { return null; }
function BannerDark() { return null; }
/* ---------- sidebar ---------- */
function Sidebar({ mini, group, items, active }) {
const list = items || ["Введение в Claude Code", "Настройка проекта с нуля", "MCP-серверы и память", "Автоматизация видео", "Архитектура памяти агента"];
const act = active == null ? 1 : active;
return (
);
}
/* ---------- end CTA ---------- */
// VOX: EdgeLab community-CTA в конце статьи убран. Заход в продукт — мягко, внутри текста.
function CtaDark() { return null; }
function CtaLime() { return null; }
/* ---------- rich content blocks ---------- */
function Callout({ children, label }) {
return (
{caption || "Схема: путь от запроса до правок в репозитории"}
);
}
function VideoEmbed({ title }) {
return (
YouTube
{title || "Инструкция · VOX"}
Видео с YouTube встраивается в статью, 16:9, грузится по клику
);
}
function StepsGuide({ steps }) {
const list = steps || [
{ t: "Установи Claude Code", d: "Один пакет и вход по ключу – пара минут." },
{ t: "Опиши проект в CLAUDE.md", d: "Правила, стек и доверенные команды в одном файле." },
{ t: "Подключи MCP-серверы", d: "filesystem и git закрывают большинство задач." },
];
return (
{list.map((s, i) => (
{String(i + 1).padStart(2, "0")}
{s.t}
{s.d}
))}
);
}
/* ---------- agent-native / SEO blocks (Layer 1) ---------- */
function Tldr({ text }) {
return (
Коротко
{text || "Если совсем коротко: я завёл один файл с описанием проекта и парой доверенных команд — этого хватило, чтобы агент перестал гадать. Всё остальное я добавлял по мере надобности."}
);
}
function TestedBadge({ text }) {
return (
{text || "Протестировано на Claude Code v1.2 · 12 июня 2026"}
);
}
function Faq({ qa }) {
const list = qa || [
["Нужно ли уметь программировать?", "Нет. Claude Code берёт рутину на себя – но базовое понимание проекта помогает направлять агента."],
["Какие MCP-серверы ставить первыми?", "filesystem и git закрывают большинство задач. Остальное добавляй под конкретную задачу, а не заранее."],
["Это работает с Codex?", "Да, подход тот же: правила проекта плюс доверенные команды. Отличаются детали запуска."],
];
return (
Частые вопросы
{list.map(([q, a], i) => ({q}
{a}
))}
);
}
function Changelog({ items }) {
const list = items || [
["12.06.2026", "Обновлено под Claude Code v1.2, добавлен раздел про MCP-память."],
["02.05.2026", "Добавлена блок-схема потока и видео-разбор."],
["18.04.2026", "Первая публикация."],
];
return (
История изменений
{list.map(([d, t], i) => (
{d}{t}
))}
);
}
/* ---------- real guides data (from MDX via scripts/build-guides-data.mjs) ---------- */
/* window.__GF_GUIDES / __GF_GUIDE_BY_SLUG are injected by guides-data.js (loaded
before this file). Each entry already carries title/desc/tldr/tested/meta/level/
min/steps/outcome/toc/bodyHtml. We adapt them to the article shape the design
engine expects (side nav, docsnav prev/next, dangerouslySetInnerHTML body). */
const GF_GUIDES = (typeof window !== "undefined" && window.__GF_GUIDES) || [];
const GF_BY_SLUG = (typeof window !== "undefined" && window.__GF_GUIDE_BY_SLUG) || {};
// Category → display name + the guides that belong to it (preserve source order).
const GUIDE_CATS = [
{ id: "vpn", name: "Обход блокировок" },
{ id: "dostup", name: "Доступ к сервисам" },
{ id: "vybor", name: "Выбор VPN" },
];
function guidesInCat(catId) {
return GF_GUIDES.filter((g) => g.cat === catId);
}
// Build the full article object the design's / renders.
function guideToArticle(g) {
if (!g) return null;
const sibs = guidesInCat(g.cat);
const idx = sibs.findIndex((s) => s.slug === g.slug);
const prev = idx > 0 ? sibs[idx - 1] : (sibs.length > 1 ? sibs[sibs.length - 1] : g);
const next = idx < sibs.length - 1 ? sibs[idx + 1] : (sibs.length > 1 ? sibs[0] : g);
return {
slug: g.slug,
cur: g.cat === "claude" ? "claude" : "library",
crumbMid: g.catName,
type: g.type || "guide",
cover: g.cover || "",
title: g.title,
desc: g.desc,
tldr: g.tldr,
tested: g.tested,
meta: g.meta,
level: g.level,
side: {
group: `${g.catName} · ${sibs.length}`,
items: sibs.map((s) => ({ t: s.title, slug: s.slug })),
active: Math.max(0, idx),
},
toc: g.toc && g.toc.length ? g.toc : ["Введение"],
docsnav: [prev.title, next.title],
docsnavSlugs: [prev.slug, next.slug],
steps: g.steps || [],
outcome: g.outcome || "",
bodyHtml: g.bodyHtml || "",
// «Похожие материалы» (recirculation block) — resolve related slugs → full guides.
related: (g.related || []).map((s) => GF_BY_SLUG[s]).filter(Boolean),
};
}
const REAL_ARTICLES = {};
for (const g of GF_GUIDES) REAL_ARTICLES[g.slug] = guideToArticle(g);
// Pick representative guides for the three category landing pages.
function firstInCat(catId, fallbackSlug) {
const list = guidesInCat(catId);
return (list[0] && list[0].slug) || fallbackSlug;
}
/* ---------- content body ---------- */
const ARTICLES = {
claude: {
cur: "claude", crumbMid: "Claude Code",
title: "Как я собрал рабочее окружение за один вечер",
desc: "История о том, как Claude Code из красивой игрушки в демо превратился в инструмент, которому я доверяю реальную работу. Без волшебства — только то, что у меня действительно сработало.",
tldr: "Если совсем коротко: я завёл один файл с описанием проекта и парой доверенных команд — этого хватило, чтобы агент перестал гадать. Всё остальное добавлял по мере надобности.",
tested: "Протестировано на Claude Code v1.2 · 12 июня 2026",
meta: "ОБНОВЛЕНО 12 ИЮНЯ 2026 · 9 МИН ЧТЕНИЯ",
side: { group: "Claude Code · 6", items: ["Введение в Claude Code", "Рабочее окружение за вечер", "MCP-серверы и память", "Доверенные команды", "Большой репозиторий", "Память агента"], active: 1 },
toc: ["Агент и контекст", "Инструменты и привычки", "Доверие шаг за шагом", "Обратная связь"],
docsnav: ["Введение в Claude Code", "MCP-серверы и память"],
intro: (
С чего всё началось
Прошло чуть больше полугода с тех пор, как Claude Code появился в моей работе — и за это время он научил меня куче вещей. Не он сам, конечно, а сам процесс: я набил столько шишек, что грех не поделиться.
Главное, что я понял: агенты вроде Claude Code или Hermes падают не на интеллекте, а на архитектуре. Чаще всего у меня ломалось не потому, что модель «тупая», а потому что я не дал ей контекст, и инструменты дрались между собой.
Ниже — ключевые уроки, которые я вынес за это время, чтобы вам не пришлось повторять мои ошибки. Давайте разберём по порядку.
),
body: (
1. Агент не тупой — ему просто не хватает контекста
Первые недели я делал классическую ошибку: кидал задачу без единого слова о проекте и злился, что агент переспрашивает очевидное. Это всё равно что посадить нового человека за чужой код и уйти пить кофе — а потом удивляться, что он сделал не то.
Однажды вечером я потратил полчаса не на саму задачу, а на то, чтобы по-человечески описать проект: что это, по каким правилам мы тут живём и какие команды можно выполнять без лишних вопросов. Тот же Claude Code, та же модель — но он перестал гадать и начал попадать с первого раза.
Что я понял: модель решает всё меньше. Гораздо важнее контекст, который ты ей даёшь. Если упростить, весь путь от моей просьбы до правок в коде укладывается в простую цепочку:
2. Важнее модели — инструменты и привычки
Когда контекст появился, всплыла вторая проблема: агент хватался не за те инструменты. Лез искать в интернете то, что лежало в соседнем файле, или запускал тяжёлую команду там, где хватило бы пары строк.
Я перестал ждать, что он сам угадает, и начал прямо подсказывать: для этой задачи — вот это, для той — вот то. Звучит банально, но именно это сэкономило мне больше всего времени и нервов.
Не пытайтесь подключить всё сразу. Я начал с самого необходимого и добавлял остальное только тогда, когда реально упирался в задачу — так контекст не превращается в свалку.
3. Доверие строится на маленьких шагах
Сначала я перепроверял за агентом каждую строчку — и, честно, это убивало весь смысл. Доверие пришло не сразу: я давал ему задачи поменьше, смотрел на результат, и постепенно отпускал поводья.
Проще один раз увидеть, чем сто раз прочитать — вот короткое видео, где агент правит код за пару минут, пока я просто наблюдаю.
Что я понял: доверие к агенту — это не вера, а привычка. Она нарабатывается на маленьких задачах, где цена ошибки невелика.
4. Обратная связь — лучший способ его обучить
Самым полезным оказалось не идеально сформулировать задачу с первого раза, а быстро поправлять. Агент сделал — я тут же сказал, что не так, он запомнил это как правило, и следующий результат стал точнее. Маленькая петля, которая работает лучше любых длинных инструкций.
Если свести всё к простому чек-листу, с которого стоит начать, — вот он, проверенный на моих проектах:
Чем всё закончилось
Сейчас Claude Code — это не игрушка из демо, а инструмент, которому я доверяю реальную работу. Он не стал умнее за эти полгода. Умнее стал я — в том, как с ним разговаривать.
Если вы только начинаете, не пытайтесь повторить всё сразу. Возьмите один урок, попробуйте на своём проекте — и дальше пойдёт само.
),
faq: [
["Нужно ли уметь программировать?", "Нет. Claude Code берёт рутину на себя — но базовое понимание проекта помогает направлять агента."],
["Какие MCP-серверы ставить первыми?", "filesystem и git закрывают большинство задач. Остальное добавляй под конкретную задачу, а не заранее."],
["Это работает с Codex?", "Да, подход тот же: правила проекта плюс доверенные команды. Отличаются детали запуска."],
],
changelog: [
["12.06.2026", "Обновлено под Claude Code v1.2, добавлен раздел про MCP-память."],
["02.05.2026", "Добавлена блок-схема потока и видео-разбор."],
["18.04.2026", "Первая публикация."],
],
},
hermes: {
cur: "hermes", crumbMid: "Hermes",
title: "Hermes стал моим личным аналитиком",
desc: "Три месяца я учил агента читать рынок и собирать сводки за меня. Честная история о том, что получилось, а что пока нет — без обещаний лёгких денег.",
tldr: "Если совсем коротко: я перестал искать «самую умную» модель и занялся процессом — утренние сводки, петля обратной связи и общая память. Это дало куда больше, чем любая смена модели.",
tested: "Протестировано на Hermes 0.9 · 10 июня 2026",
meta: "ОБНОВЛЕНО 10 ИЮНЯ 2026 · 8 МИН ЧТЕНИЯ",
side: { group: "Hermes · 5", items: ["Введение в Hermes", "Личный аналитик", "Утренние сводки", "Петля обратной связи", "Память команды"], active: 1 },
toc: ["С чего начал", "Утренние сводки", "Петля обратной связи", "Что в итоге"],
docsnav: ["Введение в Hermes", "Память команды"],
intro: (
С чего всё началось
Я не трейдер и не аналитик. Но каждое утро мне нужно было быстро понимать, что происходит в индустрии — и на это уходил час, который я ненавидел. Тогда я решил переложить эту рутину на Hermes.
Первый месяц был провальным: агент выдавал простыни текста, в которых тонула суть. Я чуть не бросил. Помогло не «более умная модель», а то, что я наконец перестроил процесс.
Ниже — три вещи, которые превратили Hermes из болтливого помощника в аналитика, которому я доверяю утро. Разберём по порядку.
),
body: (
1. Дело было не в модели
Я перепробовал кучу моделей в надежде, что какая-то окажется «умнее» и сама начнёт выдавать то, что нужно. Спойлер: не начала. Разница между ними оказалась куда меньше, чем разница между «объяснил, что мне нужно» и «не объяснил».
Как только я описал, какие источники читать, на что смотреть и в каком виде давать ответ, — качество подскочило на той же самой модели. Путь от запроса до готовой сводки у меня выглядит так:
Что я понял: гнаться за самой умной моделью — тупик. Сначала наладь процесс, и тогда даже простая модель работает на тебя.
2. Ритуал важнее разовых запросов
Переломный момент — когда сводки перестали быть разовыми. Я завёл привычку: каждое утро в 10:00 Hermes присылает короткий разбор по тем источникам, что мы вместе отобрали. Не «когда вспомню», а по расписанию.
Вот как это выглядит вживую — короткое видео, где из десятка источников рождается сводка на пару абзацев.
Что я понял: ценность не в одном гениальном ответе, а в привычке. Регулярность бьёт интеллект.
3. Петля обратной связи всё меняет
Самое полезное — быстро поправлять. Hermes выдал, я сказал «вот это лишнее, а вот этого не хватает», он запомнил это как правило, и следующая сводка стала точнее. За пару недель ответы стали такими, будто их собирал я сам, только без потраченного часа.
Если свести всё к простому ритуалу, вот он:
Чем всё закончилось
Сейчас я открываю утреннюю сводку как кофе — по привычке. Hermes не предсказывает будущее и не делает меня богаче за ночь. Он просто экономит мне час каждый день и держит в курсе. Для меня этого более чем достаточно.
),
faq: [
["Hermes правда помогает зарабатывать?", "Он помогает быть в курсе и экономит время. Решения по-прежнему за вами — это инструмент, а не оракул."],
["Сколько времени на настройку?", "Базовый ритуал собрал за вечер. Дальше он сам становится точнее за счёт правок."],
["Нужны ли особые знания?", "Нет. Важнее понимать, какие источники вам действительно полезны."],
],
changelog: [
["10.06.2026", "Добавил раздел про петлю обратной связи."],
["28.05.2026", "Перевёл сводки на расписание."],
["15.05.2026", "Первая публикация."],
],
},
codex: {
cur: "codex", crumbMid: "Codex",
title: "Codex на практике: чего я не ожидал",
desc: "Сел попробовать на вечер — остался на неделю. Рассказываю, где Codex приятно удивил, а где пришлось перестроить привычки. Личные наблюдения, а не пересказ документации.",
tldr: "Если совсем коротко: Codex отлично закрывает рутину — ревью, мелкие правки, автоисправления. Но доступы и песочницу стоит настроить с самого начала, иначе нервов уйдёт больше, чем сэкономишь.",
tested: "Протестировано на Codex · 8 июня 2026",
meta: "ОБНОВЛЕНО 8 ИЮНЯ 2026 · 7 МИН ЧТЕНИЯ",
side: { group: "Codex · 4", items: ["Первый запуск Codex", "Ревью пул-реквестов", "Песочница и доступы", "Codex против Claude Code"], active: 0 },
toc: ["Первое впечатление", "Ревью на автомате", "Песочница", "Что в итоге"],
docsnav: ["Первый запуск Codex", "Codex против Claude Code"],
intro: (
Первое впечатление
Я подходил к Codex со здоровым скепсисом: ещё один агент, который обещает писать код за меня. Сел попробовать на один вечер — а закрыл ноутбук только через неделю, потому что втянулся.
Удивило не то, что он пишет код, а то, как аккуратно он берёт на себя скучное: ревью, мелкие правки, рутинные проверки. Но без пары настроек в начале я бы намучился.
Ниже — что зашло сразу, а что пришлось перенастроить под себя. Разберём по порядку.
),
body: (
1. Лучше всего заходит на рутине
Я не стал сразу поручать Codex большие фичи. Начал с мелочи: причесать функцию, поправить тесты, пройтись по замечаниям. И вот тут он оказался незаменим — то, на что у меня уходил час скуки, он делает за минуты.
Если упростить, его путь от задачи до готового результата выглядит так:
Что я понял: не нужно сразу поручать агенту главное. Отдай ему скучное — и он окупится в первый же день.
2. Ревью на автомате — то, ради чего стоит остаться
Главное открытие — Codex как первый ревьюер. Он проходит по пул-реквесту раньше меня и ловит мелочи, на которые у живого человека уже не хватает внимания. Я прихожу к ревью, когда половина замечаний уже снята.
Вот как это выглядит на практике — короткое видео разбора одного PR.
Что я понял: агент-ревьюер экономит не столько время, сколько внимание команды для действительно важного.
3. Песочницу и доступы — сразу
Единственное, обо что я споткнулся, — доступы. Поначалу дал слишком много, потом испугался и закрутил гайки так, что он перестал быть полезным. Золотая середина нашлась быстро, но лучше продумать её заранее.
Вот короткий чек-лист, с которого я бы советовал начать:
Чем всё закончилось
Codex не заменил мне Claude Code — они хорошо делят обязанности. Codex я держу на рутине и ревью, Claude Code — на задачах, где нужно глубже погрузиться в проект. Вместе они закрывают почти всё, и это оказалось удобнее, чем один агент на всё.
),
faq: [
["Codex заменит Claude Code?", "Скорее дополнит. Мне удобнее держать обоих: один на рутине и ревью, другой на глубоких задачах."],
["С чего безопасно начать?", "С песочницы и минимальных доступов. Дальше расширяешь по мере доверия."],
["Где Codex полезнее всего?", "На ревью пул-реквестов и мелких правках — там он экономит больше всего внимания."],
],
changelog: [
["08.06.2026", "Добавил чек-лист по доступам и песочнице."],
["30.05.2026", "Раздел про ревью пул-реквестов."],
["20.05.2026", "Первая публикация."],
],
},
};
// Static «Скопировать страницу» button at the top of the article (copies the page's
// markdown twin /guides/.md for pasting into an AI agent). Replaces the old
// floating bottom FAB — sits statically next to the title, does NOT follow scroll.
function CopyPageButton({ slug }) {
const [copied, setCopied] = useState(false);
const onClick = () => {
const done = () => { setCopied(true); window.clearTimeout(window.__cpT); window.__cpT = window.setTimeout(() => setCopied(false), 2400); };
const doCopy = (text) => {
if (!text) return;
if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(text).then(done).catch(() => {});
else { const ta = document.createElement("textarea"); ta.value = text; ta.style.position = "fixed"; ta.style.opacity = "0"; document.body.appendChild(ta); ta.select(); try { if (document.execCommand("copy")) done(); } catch (e) {} document.body.removeChild(ta); }
};
if (slug) fetch("/guides/" + slug + ".md").then((r) => (r.ok ? r.text() : Promise.reject())).then(doCopy).catch(() => { const a = document.querySelector(".gp-article-body, .m-content"); if (a) doCopy(a.innerText); });
else { const a = document.querySelector(".gp-article-body, .m-content"); if (a) doCopy(a.innerText); }
};
return (
);
}
function CopyButton() {
const [copied, setCopied] = useState(false);
return (
);
}
// Clean card-description truncation: cut at a sentence end (.!?) near the limit, else
// at the last word boundary, then «…». No mid-word cuts, no «....» artefacts.
function cardDesc(desc, maxLen = 88) {
const s = String(desc || "").replace(/\s+/g, " ").trim();
if (s.length <= maxLen) return s;
const slice = s.slice(0, maxLen);
const sent = Math.max(slice.lastIndexOf(". "), slice.lastIndexOf("! "), slice.lastIndexOf("? "));
if (sent >= maxLen * 0.55) return slice.slice(0, sent + 1); // clean sentence end
const sp = slice.lastIndexOf(" ");
return (sp > 0 ? slice.slice(0, sp) : slice).replace(/[\s,.;:–—-]+$/, "") + "…";
}
// «Похожие материалы» — recirculation block at the end of an article/guide. Plain
// covers (NOT /: those hydrate separately and may snapshot
// empty during prerender). Cards link to sibling guides → raise recirculation.
function RelatedGuides({ items }) {
if (!items || items.length === 0) return null;
return (
);
}
function Toc({ items }) {
const list = items || ["Агент и контекст", "Инструменты и привычки", "Доверие шаг за шагом", "Обратная связь"];
// scroll-spy: highlight the section the reader is currently in. Each real H2 in
// the article body has id="sec-" (build-guides-data) matching list index i.
const [active, setActive] = React.useState(0);
React.useEffect(() => {
const heads = Array.prototype.slice.call(document.querySelectorAll(".gp-article-body h2[id]"));
if (!heads.length) return;
const onScroll = () => {
let cur = 0;
for (let i = 0; i < heads.length; i++) {
if (heads[i].getBoundingClientRect().top <= 90) cur = i; else break;
}
setActive(cur);
};
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
const onClick = (e, i) => {
const el = document.getElementById("sec-" + i);
if (el) { e.preventDefault(); el.scrollIntoView({ behavior: "smooth", block: "start" }); }
};
return (
История о том, как Claude Code из игрушки в демо превратился в инструмент, которому я доверяю реальную работу.
ОБНОВЛЕНО 12 ИЮНЯ 2026 · 9 МИН
С чего всё началось
Полгода назад я считал Claude Code игрушкой: в демо красиво, в реальном проекте — бесполезно. Пока однажды не потратил полчаса на то, чтобы по-человечески объяснить агенту проект.
Лучше один раз увидеть
Короткое видео: агент правит код за пару минут, пока я наблюдаю.
)}
{a.type !== "article" && a.outcome && {a.outcome}}
) : (
{a.intro}
{a.body}
)}
{/* VOX: EdgeLab mobile community-CTA убран — заход в продукт внутри текста статьи */}
↑↓навигация↵открытьescзакрыть{results.length} из {ALL_GUIDES.length}
);
}
// Catalog sort modes. "new" is the default — publishedAt desc, i.e. the toolbar's
// long-standing «Сначала новые» label, now actually applied (before this it was a
// decorative span and the grid showed source order). Search results keep MiniSearch
// relevance order, so sorting applies only with an empty query.
const SORTS = [
{ id: "new", label: "Сначала новые" },
{ id: "time", label: "Быстрые сначала" },
{ id: "cat", label: "По категории" },
];
function sortGuides(list, mode) {
const a = list.slice();
// «Скоро» placeholders never lead the date/time views (they'd otherwise top
// «новые» by newest date and «быстрые» by min reading time, pushing real
// content + the crawlable above-the-fold below stubs). In «cat» they stay in
// their category. stub === 1 sorts after stub === 0.
const stub = (x, y) => (x.stub ? 1 : 0) - (y.stub ? 1 : 0);
if (mode === "time") return a.sort((x, y) => stub(x, y) || x.min - y.min || x.title.localeCompare(y.title, "ru"));
if (mode === "cat") return a.sort((x, y) => x.catName.localeCompare(y.catName, "ru") || (y.publishedAt || "").localeCompare(x.publishedAt || ""));
return a.sort((x, y) => stub(x, y) || (y.publishedAt || y.updatedAt || "").localeCompare(x.publishedAt || x.updatedAt || ""));
}
function Catalog({ initialTheme, mobile, initialCmdk }) {
const [theme, setTheme] = useState(getStoredTheme(initialTheme));
const night = theme === "dark";
const [cat, setCat] = useState("all");
const [level, setLevel] = useState("all");
const [q, setQ] = useState("");
const [cmdk, setCmdk] = useState(initialCmdk || false);
const [sort, setSort] = useState("new");
const [sortOpen, setSortOpen] = useState(false);
const sortRef = React.useRef(null);
// Full-text search drives the base set; facets (cat + level) filter the
// searched results. Empty query → all guides, ordered by the chosen sort.
const base = React.useMemo(() => searchGuides(q), [q]);
const filtered = base.filter((g) => (cat === "all" || g.cat === cat) && (level === "all" || g.level === level));
const hasQuery = q.trim().length > 0;
// Only 11 real guides — show them all (no demo pagination on a small set).
// Sort applies to the empty-query catalog; with a query we keep relevance order.
const cards = hasQuery ? filtered : sortGuides(filtered, sort);
const total = filtered.length;
React.useEffect(() => {
if (!sortOpen) return;
const onDoc = (e) => { if (sortRef.current && !sortRef.current.contains(e.target)) setSortOpen(false); };
document.addEventListener("click", onDoc);
return () => document.removeEventListener("click", onDoc);
}, [sortOpen]);
// Hide the open menu if the user starts a search (the toolbar swaps it out).
React.useEffect(() => { if (hasQuery) setSortOpen(false); }, [hasQuery]);
// No-results fallback: surface a few first guides so the page is never empty.
const popular = ALL_GUIDES.slice(0, 3);
// ⌘K keyboard shortcut still opens the modal palette (the inline input is primary).
React.useEffect(() => {
const onKey = (e) => {
if ((e.metaKey || e.ctrlKey) && (e.key === "k" || e.key === "K")) { e.preventDefault(); setCmdk(true); }
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, []);
return (
flipTheme(setTheme)} />
ВСЕ ИНСТРУКЦИИ
Гайды по доступу к заблокированному
Как вернуть YouTube, WhatsApp, Instagram и зарубежные сервисы в России и как выбрать рабочий VPN. Найди нужное за секунды — поиском или по категориям.
⌕
setQ(e.target.value)}
placeholder="Найти гайд по названию, теме или тексту…"
aria-label="Поиск по гайдам"
autoComplete="off"
spellCheck={false}
/>
{q ? setQ("")}>✕ : ⌘K}
);
}
/* ============================================================ */
/* ===== HOME / blog landing (editorial, human voice) ===== */
function PlayGlyph() {
return ();
}
function Slot({ id, ph, r, src, fit, position }) {
// Deployed statically (no omelette fill runtime): only render a slot that has a
// real cover image. Empty placeholders (dashed «Обложка» boxes) are hidden until
// a cover exists, so the home/category/about pages never show empty frames.
// `fit` (cover|contain|fill) is forwarded to the image-slot element so a
// pre-composed banner can be shown whole (contain) instead of cropped (cover).
if (!src) return null;
return ();
}
function HomeSecHead({ title }) {
return (
);
}
/* ===== Category page (Claude Code / Hermes / Codex) ===== */
const CATPAGES = {
claude: {
cur: "claude", eyebrow: "Claude Code",
title: "Claude Code без боли",
lead: "Личный опыт работы с агентом, который правит код сам: настройка, привычки, разборы реальных задач — по-человечески, без сухих инструкций.",
featTag: "Claude Code · личный опыт",
featTitle: "Как я собрал рабочее окружение за один вечер",
featDesc: "Никакого волшебства — только то, что реально сработало. Что сломалось по дороге и почему полчаса настройки окупились на первой же задаче.",
featMeta: "12 ИЮНЯ · 9 МИН ЧТЕНИЯ",
list: [
{ t: "Память, которая не забывает", d: "Почему агент наконец перестал переспрашивать одно и то же.", m: "18 МАЯ · 6 МИН" },
{ t: "День, когда агент написал свой первый скилл", d: "Маленький момент, после которого всё пошло иначе.", m: "14 МАЯ · 5 МИН" },
{ t: "Доверенные команды: меньше вопросов", d: "Как я перестал подтверждать каждый шаг и стал доверять агенту.", m: "9 МАЯ · 6 МИН" },
{ t: "Большой репозиторий и контекст", d: "Что помогло агенту не теряться в проекте на сотни файлов.", m: "2 МАЯ · 8 МИН" },
],
articles: [
{ t: "Почему я перестал гнаться за самой умной моделью", d: "Оказалось, дело вообще не в модели.", m: "3 ИЮНЯ · 6 МИН" },
{ t: "Инструменты решают больше, чем кажется", d: "Один правильный инструмент сэкономил мне неделю.", m: "27 МАЯ · 5 МИН" },
{ t: "Мой CLAUDE.md, который я ношу из проекта в проект", d: "Что в нём лежит и почему он так экономит время.", m: "20 МАЯ · 7 МИН" },
],
},
hermes: {
cur: "hermes", eyebrow: "Hermes",
title: "Hermes — личный аналитик",
lead: "История о том, как я три месяца учил агента читать рынок и собирать утренние сводки за меня. Что получилось, а что — нет.",
featTag: "Hermes · личный опыт",
featTitle: "Hermes стал моим аналитиком",
featDesc: "Три месяца я учил агента читать рынок за меня. Рассказываю, что из этого вышло — без прикрас и обещаний лёгких денег.",
featMeta: "25 МАЯ · 8 МИН ЧТЕНИЯ",
list: [
{ t: "Утренние сводки в 10:00", d: "Как я собрал ритуал, который экономит мне час каждый день.", m: "21 МАЯ · 6 МИН" },
{ t: "Обратная связь = лучший способ обучить агента", d: "Шесть шагов петли, после которых ответы стали точнее.", m: "16 МАЯ · 7 МИН" },
{ t: "Эхо-камера: честно о проблеме", d: "Где агент начинает повторять одно и то же и что я с этим делаю.", m: "10 МАЯ · 5 МИН" },
{ t: "Маршрутизация задач между агентами", d: "Кто за что отвечает в моей связке и почему так.", m: "3 МАЯ · 6 МИН" },
],
articles: [
{ t: "Открытость победит — и вот почему", d: "Честный взгляд на то, куда движется индустрия.", m: "21 МАЯ · 7 МИН" },
{ t: "Как я перестал бояться, что агент ошибётся", d: "Спокойствие приходит, когда у тебя есть петля проверки.", m: "12 МАЯ · 5 МИН" },
{ t: "Память команды: один контекст на всех", d: "Зачем агентам общая память и как её не превратить в свалку.", m: "5 МАЯ · 6 МИН" },
],
},
codex: {
cur: "codex", eyebrow: "Codex",
title: "Codex на практике",
lead: "Как я подружил Codex с реальными проектами: запуск, ревью, автоисправления. Личные наблюдения, а не пересказ документации.",
featTag: "Codex · личный опыт",
featTitle: "Первый запуск Codex: чего я не ожидал",
featDesc: "Сел попробовать на вечер — остался на неделю. Рассказываю, где Codex приятно удивил, а где пришлось перестроить привычки.",
featMeta: "8 ИЮНЯ · 7 МИН ЧТЕНИЯ",
list: [
{ t: "Codex в ревью пул-реквестов", d: "Как агент стал моим первым ревьюером и что это дало команде.", m: "1 ИЮНЯ · 6 МИН" },
{ t: "Автоисправление багов: где предел", d: "Что Codex чинит сам, а где всё ещё нужен человек.", m: "24 МАЯ · 5 МИН" },
{ t: "Песочница и доступы по уму", d: "Как я настроил безопасный запуск без лишней паранойи.", m: "17 МАЯ · 6 МИН" },
{ t: "Codex против Claude Code: мой выбор", d: "Когда беру одно, когда другое — без священных войн.", m: "10 МАЯ · 8 МИН" },
],
articles: [
{ t: "Почему я держу два агента, а не одного", d: "Разные задачи — разные сильные стороны.", m: "30 МАЯ · 6 МИН" },
{ t: "CI и агент: автоматизация без сюрпризов", d: "Как я пустил Codex в пайплайн и спокойно сплю.", m: "23 МАЯ · 7 МИН" },
{ t: "Промпты для Codex, которые реально работают", d: "Несколько приёмов, проверенных на своих проектах.", m: "15 МАЯ · 5 МИН" },
],
},
};
function CategoryPage({ cat, initialTheme, mobile, idp }) {
const c = CATPAGES[cat] || CATPAGES.claude;
const p = idp || cat;
const [theme, setTheme] = useState(getStoredTheme(initialTheme));
const night = theme === "dark";
return (
);
}
/* ===== About page ===== */
function AboutPage({ initialTheme, mobile }) {
const [theme, setTheme] = useState(getStoredTheme(initialTheme));
const night = theme === "dark";
return (
flipTheme(setTheme)} />
О проекте
{BRAND_NAME} — про доступ к заблокированному
Понятные разборы: как в 2026 году вернуть YouTube, WhatsApp, Instagram и другие сервисы в России и как выбрать VPN, который действительно работает. Простым языком, без воды и без обещаний.
В 2026 году доступ к привычным сервисам в России стал непредсказуемым: то YouTube не грузится, то отваливается WhatsApp, то перестаёт подключаться VPN. Мы разбираем, что происходит на самом деле и что с этим можно сделать — простым языком, без технических заморочек.
Пишем честно: объясняем причину, показываем рабочие шаги и не обещаем невозможного. Если чего-то не может ни один VPN — так и говорим.
Что внутри
Актуальные инструкции
Под реальную ситуацию 2026 года, а не общие советы пятилетней давности.
Для iPhone и Android
Пошагово и понятно, без строчек кода и лишних терминов.
Честно, без обещаний
Называем и то, что работает, и то, где есть ограничения.
Меню слева. По тапу на бургер слева выезжает drawer, фон затемняется. Внутри – разделы и список гайдов. Закрытие по «✕» или тапу по затемнению.
Тёмная тема. Тот же drawer в тёмном стиле – фон #141416, lime-акцент на активном пункте, затемнение фона глубже.
Кнопка для агента. При прокрутке ниже 1500px всплывает кнопка «Скопировать для агента» – копирует всю страницу в Markdown, чтобы отправить агенту. Кликни – покажет подтверждение.
Командная палитра. ⌘K (или клик по строке поиска) открывает палитру: живой поиск по всем 500 гайдам, сгруппированный по категориям. Печатай – список фильтруется.
Держи шаги короткими: одно действие – один пункт. Так гайд легче сканировать и он лучше ранжируется.
);
}
// Delegated copy-to-clipboard for code blocks + prompt callouts (M1). Buttons
// (.copy-btn) are baked into the static guide HTML by build-guides-data.mjs; a
// single document-level listener handles all of them (works after re-mount).
// Analytics goal: fire when any «Вступить» CTA is clicked. The CTA links carry
// utm_medium= (header/banner/cta/footer/about); we read it so GA4 +
// Yandex.Metrika see WHICH placement converts. Capture-phase → fires before the
// page navigates away. Guarded: no-op during prerender (no gtag/ym) or if blocked.
if (typeof document !== "undefined" && !window.__gfCtaBound) {
window.__gfCtaBound = true;
document.addEventListener("click", (e) => {
// Scope to the community-join destination only (edgelab.space). Without the
// host filter, an editorial workshop link (edgelab.su, utm_campaign=workshop)
// in a guide body would wrongly fire the join goal and pollute placements.
const a = e.target.closest && e.target.closest(`a[href*="${FUNNEL_HOST}"][href*="utm_medium="]`);
if (!a) return;
const placement = (a.href.match(/utm_medium=([^&]+)/) || [])[1] || "unknown";
try { if (typeof window.gtag === "function") window.gtag("event", "join_click", { placement: placement, link_url: a.href }); } catch (err) {}
try { if (typeof window.ym === "function") window.ym(109832554, "reachGoal", "join_click", { placement: placement }); } catch (err) {}
}, true);
}
if (typeof document !== "undefined" && !window.__gfCopyBound) {
window.__gfCopyBound = true;
document.addEventListener("click", (e) => {
const btn = e.target.closest && e.target.closest(".copy-btn");
if (!btn) return;
const kind = btn.getAttribute("data-copy");
let text = "";
if (kind === "code") {
const pre = btn.closest(".code-block") && btn.closest(".code-block").querySelector("pre");
text = pre ? pre.innerText : "";
} else if (kind === "prompt") {
// Copy the WHOLE prompt body (paragraphs + ol/ul lists), not just
—
// numbered task lists were being silently dropped. Clone, strip the
// «ПРОМПТ» eyebrow + the button, then take innerText.
const cal = btn.closest(".blk-callout");
if (cal) {
const clone = cal.cloneNode(true);
clone.querySelectorAll(".gp-eyebrow, .copy-btn").forEach((n) => n.remove());
text = clone.innerText.trim();
}
}
if (!text) return;
const done = () => {
if (!btn.__orig) btn.__orig = btn.innerHTML;
btn.classList.add("copied");
btn.innerHTML = '';
window.clearTimeout(btn.__t);
btn.__t = window.setTimeout(() => {
btn.classList.remove("copied");
btn.innerHTML = btn.__orig;
}, 2000);
};
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(done).catch(() => {});
} else {
// fallback for non-secure contexts
const ta = document.createElement("textarea");
ta.value = text; ta.style.position = "fixed"; ta.style.opacity = "0";
document.body.appendChild(ta); ta.select();
try { if (document.execCommand("copy")) done(); } catch (err) { /* noop */ }
document.body.removeChild(ta);
}
});
}
// M1b: fixed «Скопировать страницу» button on guide pages — copies the guide's
// markdown twin (/guides/.md) for pasting into an AI agent. React-managed
// (single instance, no prerender duplication). Falls back to the rendered
// article text if the .md fetch fails. Dark button works on light + dark themes.
function PageCopyFab({ slug }) {
const [copied, setCopied] = useState(false);
if (!slug) return null;
const onClick = () => {
const done = () => {
setCopied(true);
window.clearTimeout(window.__pfT);
window.__pfT = window.setTimeout(() => setCopied(false), 2600);
};
const doCopy = (text) => {
if (!text) return;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(done).catch(() => {});
} else {
const ta = document.createElement("textarea");
ta.value = text; ta.style.position = "fixed"; ta.style.opacity = "0";
document.body.appendChild(ta); ta.select();
try { if (document.execCommand("copy")) done(); } catch (e) { /* noop */ }
document.body.removeChild(ta);
}
};
fetch("/guides/" + slug + ".md")
.then((r) => (r.ok ? r.text() : Promise.reject()))
.then(doCopy)
.catch(() => {
const a = document.querySelector(".gp-article-body, .m-content");
if (a) doCopy(a.innerText);
});
};
return (
);
}
const __gfRoot = ReactDOM.createRoot(document.getElementById("root"));
if (window.__GF_VIEW === "variantC") {
// Standalone guide page shows the first real guide.
const art = REAL_ARTICLES[GF_GUIDES[0] && GF_GUIDES[0].slug] || guideToArticle(GF_GUIDES[0]);
__gfRoot.render(
} cta={} rich={true} />
);
} else if (window.__GF_VIEW === "home") {
__gfRoot.render(
);
} else if (window.__GF_VIEW === "category") {
// Each category route maps to a representative REAL guide in that category.
// Hermes/Codex now have placeholder intro guides (cat=hermes/codex).
const CAT_TO_SLUG = {
claude: firstInCat("claude", GF_GUIDES[0] && GF_GUIDES[0].slug),
hermes: firstInCat("hermes", GF_GUIDES[0] && GF_GUIDES[0].slug),
codex: firstInCat("codex", GF_GUIDES[0] && GF_GUIDES[0].slug),
};
const slug = CAT_TO_SLUG[window.__GF_CAT] || CAT_TO_SLUG.claude;
const art = REAL_ARTICLES[slug] || guideToArticle(GF_GUIDES[0]);
__gfRoot.render(
} cta={} rich={true} />
);
} else if (window.__GF_VIEW === "about") {
__gfRoot.render(
);
} else if (window.__GF_VIEW === "library") {
__gfRoot.render(
);
} else if (window.__GF_VIEW === "mobile") {
// Route resolution order: live URL hash (interactive nav in mobile.html) wins;
// then __GF_ROUTE (baked into the prerendered build/m/.html so it
// re-mounts to the right screen even though dynamic serving strips the hash);
// then __GF_CAT; default home. __GF_ROUTE is a reserved route name
// (home/library/claude/hermes/codex/about) or a real guide slug — the legacy
// "guide" alias below is hash-only and never baked.
const v = (window.location.hash || "").replace("#", "") || window.__GF_ROUTE || window.__GF_CAT || "home";
const firstReal = REAL_ARTICLES[GF_GUIDES[0] && GF_GUIDES[0].slug];
const catReal = (cat) => REAL_ARTICLES[firstInCat(cat, GF_GUIDES[0] && GF_GUIDES[0].slug)];
const bySlug = REAL_ARTICLES[v];
const inner = bySlug ?
: v === "claude" ?
: v === "hermes" ?
: v === "codex" ?
: v === "guide" ?
: v === "about" ?
: v === "library" ?
: ;
// page-copy FAB only on mobile GUIDE/category article views
const mGuideSlug = bySlug ? v
: (v === "claude" || v === "hermes" || v === "codex") ? firstInCat(v, GF_GUIDES[0] && GF_GUIDES[0].slug)
: v === "guide" ? (GF_GUIDES[0] && GF_GUIDES[0].slug)
: null;
__gfRoot.render(