Skip to Content

kw_llms_txt: як Odoo віддає /llms.txt — і чого цей файл не гарантує

llms.txt — запропонований формат: текстовий файл у Markdown, який лежить в корені сайту поруч з robots.txt і дає короткий опис ресурсу плюс список посилань. Специфікацію опублікував Jeremy Howard 3 вересня 2024 року на llmstxt.org. Ідея — дати AI-агенту стислий орієнтир замість обходу всього сайту; специфікація описує читання файлу як «on demand, when an agent needs information» — за запитом, коли агенту потрібна інформація. Автор спеки пише, що очікував переважно inference-використання, а не навчання моделей, але прямо визнає: «training runs could take advantage of the information too» — тобто повністю тренувальний сценарій специфікація не виключає.

Важливо! Специфікація нічого не каже про те, що ChatGPT, Claude, Perplexity чи Google використовують цей файл для відповідей чи ранжування в пошуку — першоджерела від самих цих компаній я не знайшов. Вважайте llms.txt рекомендацією для тих агентів, які самі вирішили його читати, не гарантованим сигналом для видачі.

Ну ок. Модуль kw_llms_txt вирішує вужчу задачу — дає точку, куди покласти такий файл на Odoo-сайті, і маршрут, який його віддасть.

Що зберігає вміст

Усе поле — одне текстове поле на моделі website:

# kw_llms_txt/models/website.py:4-12
class Website(models.Model):
    _inherit = 'website'

    llms_txt = fields.Text(
        string='llms.txt',
        translate=False,
        groups='website.group_website_designer',
        help='Markdown content served at /llms.txt (llmstxt.org format).',
    )

translate=False означає, що мовних варіантів немає — один текст на весь сайт, незалежно від того, скільки мов у ньому увімкнено.

Маршрут

# kw_llms_txt/controllers/main.py:1-17
from odoo.http import request
from odoo import http


class KwLlmsTxt(http.Controller):

    @http.route(
        '/llms.txt', type='http', auth='public', website=True,
        multilang=False, sitemap=False)
    def llms_txt(self, **kwargs):
        content = request.website.sudo().llms_txt
        if not content:
            raise request.not_found()
        return request.make_response(
            content,
            headers=[('Content-Type', 'text/plain; charset=utf-8')])

Контролер бере request.website — сайт, який Odoo вже визначив за доменом запиту — і читає поле через sudo(), бо воно закрите групою website.group_website_designer: без sudo() анонімний відвідувач отримав би AccessError замість файлу. Порожній рядок або False — і маршрут повертає 404, а не порожню сторінку.

Неправильно очікувати, що модуль сам збере вміст зі сторінок, меню, блогу чи товарів. Правильно розуміти: це поле й маршрут, вміст пишете ви самі — руками або скриптом через write(). Генерації, синхронізації з sitemap чи кешування тут немає: кожен запит на /llms.txt — звичайне читання поточного значення поля.

Ось що поверне контролер для рядка з тестового пакета модуля:

# Site One

> Summary — café

## Docs

- [Home](https://one.example.com/): main page

Структура (H1, blockquote-резюме, H2 зі списком посилань) відповідає прикладу зі специфікації llmstxt.org — той самий приклад продубльований у формі редагування як підказка.

Хто може редагувати

# kw_llms_txt/security/ir.model.access.csv:2
access_website_llms,access.website.llms,model_website_llms,website.group_website_designer,1,1,1,0

Це право на транзієнтну модель wizard'а website.llms. Саме поле llms_txt на website захищене окремо — атрибутом groups у визначенні поля, а не через цей csv. Тест це показує: користувач без групи Website Designer вільно читає name сайту, але на llms_txt отримує AccessError (tests/test_llms_txt_access.py:41-44); і навіть користувач з правом write() на всю модель website, але без групи дизайнера, на записі llms_txt падає так само (tests/test_llms_txt_access.py:46-53) — право на модель не замінює право на поле.

Мультисайт

Кожен сайт має власне значення поля — контролер обирає його через request.website, той самий домен-based механізм, яким Odoo визначає сайт для будь-якого запиту. У тестах два сайти з різними доменами повертають кожен свій файл, без перетину вмісту (tests/test_llms_txt_route.py:47-56).

Редагування прив'язане до сайту, вибраного в Settings: кнопка "Edit llms.txt" викликає

# kw_llms_txt/models/res_config_settings.py:7-16
def action_open_llms_txt(self):
    self.website_id._force()
    return {
        'name': _("llms.txt"),
        'view_mode': 'form',
        'res_model': 'website.llms',
        'type': 'ir.actions.act_window',
        'views': [[False, 'form']],
        'target': 'new',
    }

_force() переключає активний сайт на обраний у Settings ДО відкриття wizard'а — інакше форма показала б контент не того сайту, з яким ви щойно працювали.

Увімкнути й заповнити

  1. Заходимо в Settings → Website і вибираємо сайт (якщо їх декілька).
  2. У секції SEO, поруч із robots.txt, натискаємо «Edit llms.txt».
  3. Вводимо Markdown-текст, зберігаємо.
  4. Відкриваємо https://<домен>/llms.txt і перевіряємо.

Кнопка вставлена xpath'ом одразу після robots.txt:

<!-- kw_llms_txt/views/res_config_settings_views.xml:9-17 -->
<xpath expr="//setting[@id='robots_setting']" position="after">
    <setting id="llms_txt_setting" string="llms.txt"
             help="llms.txt: a Markdown file that gives AI assistants and LLM-based tools a concise overview of your website.">
        <div class="mt4">
            <button type="object" name="action_open_llms_txt" string="Edit llms.txt"
                    class="btn-link" icon="fa-file-text-o" noSaveDialog="true"/>
        </div>
    </setting>
</xpath>

Де межі

Модуль не читає жодної іншої моделі, крім website — немає коду, що звертався б до website.page, website.menu, товарів чи постів блогу. Автоматично протягнути в /llms.txt неопубліковану сторінку чи чернетку блогу він фізично не може, бо туди й не заглядає. Але це ж означає, що дисципліна повністю на вас: вставите в поле текст чи посилання з внутрішньої сторінки — модуль віддасть це в plain text будь-якому анонімному відвідувачу, валідації немає. І зміни на сайті в /llms.txt не відображаються самі — оновили меню, правте файл окремо.

Порожнє поле — стандартний стан щойно встановленого модуля, і тоді /llms.txt віддає 404 (це перевірено тестом, а не особливий випадок). На kitworks.systems модуль встановлений; що саме зараз у полі — дивіться на живому /llms.txt, бо вміст ручний, а не автогенерація (розділ «Де межі»).

kw_llms_txt: як Odoo віддає /llms.txt — і чого цей файл не гарантує
KitWorks, Volodymyr Karabanov 24 вересня 2026 р.
Поділитися цією публікацією
Теги
Архів