Перейти к основному содержимому

Astro Clean Template

Главная цель этого шаблона — получить на выходе чистый, читаемый и готовый к передаче код. Полезно для работы с CMS. Без зашифрованных имён классов, склеенных в нечитаемое месиво скриптов и бесконечных хешей в названиях файлов.

Установка

Развернуть можно через мой инструмент Stack:

npx @davidaganov/stack

Проблема классической сборки

Часто бывает так, что фронтендеру нужно сделать не полноценное SPA со сложной логикой, а просто качественно сверстать сайт (или набор страниц), который потом передадут для интеграции с CMS (Bitrix, WordPress или любой самопис).

Если взять для этой задачи привычный стек (Vue/Nuxt/React/Next), на выходе вы получите папку dist, с которой будет невозможно работать:

  • Непредсказуемые хеши в именах файлов: main.abc12def.js
  • Склеенные стили, которые сложно разделить или точечно переопределить
  • JSX/SFC-компоненты, которые нельзя просто открыть в браузере и посмотреть

Разработчик открывает результат и не понимает, где именно лежит нужный ему кусок логики, потому что всё собрано в один минифицированный файл. С другой стороны — писать всё на голом HTML в 2026 году или тащить в проект Gulp — это отдельный вид извращения. Хочется иметь компонентный подход, переиспользуемые лейауты и Scoped стили.

Решение

Astro Clean Template решает эту проблему. Я взял Astro как идеальный инструмент для авторинга (чтобы писать компоненты и лейауты с комфортом), но переписал процесс сборки так, чтобы на выходе получалась классическая, "олдскульная" структура файлов.

dist/index.html
<!DOCTYPE html>
<html lang="ru">
  <head>
    <link
      rel="stylesheet"
      href="/assets/style/main.css"
    />
  </head>
  <body>
    ...
    <script
      is:inline
      type="module"
      src="/assets/script/main.js"
    ></script>
  </body>
</html>

Как это работает под капотом

Один CSS-файл без препроцессоров

Все стили агрегируются в один dist/assets/style/main.css. Внутри используется нативный CSS с @import и встроенным в браузеры нестингом.

src/assets/styles/main.css
@import "./reset.css";
@import "./variables.css";

.container {
  margin-inline: auto;
  max-width: var(--max-width);

  @media (max-width: 640px) {
    padding-inline: 16px;
  }
}

Стабильные пути к скриптам

Vite по умолчанию пытается собрать и забандлить все ассеты. Я обошел это: скрипты не собираются в общий бандл. Лейаут ссылается на файл как обычно:

src/layouts/Layout.astro
<script is:inline type="module" src="/assets/script/main.js"></script>

В режиме build файлы из src/assets/script/ просто "как есть" копируются в dist/. Это сохраняет структуру модулей, оставляя код удобным для дальнейшей работы. Никаких хешей и минификации.

Режимы сборки

КомандаЧто происходит
npm run buildСборка для передачи. CSS/JS не минифицируются, HTML не сжимается. Идеально для отладки и интеграции.
npm run build:prodСборка в продакшн. Всё минифицировано, а main.js бандлится через esbuild в один компактный файл, импорты разрешаются.

Итог

Этот шаблон во многом заменяет тот самый знакомый многим "Gulp + PUG/EJS", объединяя современные технологии для авторинга (Astro, Vite) с максимально предсказуемым и чистым результатом на выходе сборки.

Если в проекте HTML/CSS/JS — это конечный продукт, а не промежуточный артефакт, — шаблон сэкономит вам массу времени.

\n \n\n","html",[164,287,288,305,327,338,347,363,378,384,394,404,411,419,425,440,455,466,475],{"__ignoreMap":162},[167,289,290,294,298,302],{"class":169,"line":108},[167,291,293],{"class":292},"sMK4o","\n",[167,306,307,310,312,315,318,321,323,325],{"class":169,"line":101},[167,308,309],{"class":292},"<",[167,311,285],{"class":296},[167,313,314],{"class":300}," lang",[167,316,317],{"class":292},"=",[167,319,320],{"class":292},"\"",[167,322,14],{"class":176},[167,324,320],{"class":292},[167,326,304],{"class":292},[167,328,330,333,336],{"class":169,"line":329},3,[167,331,332],{"class":292}," <",[167,334,335],{"class":296},"head",[167,337,304],{"class":292},[167,339,341,344],{"class":169,"line":340},4,[167,342,343],{"class":292}," <",[167,345,346],{"class":296},"link\n",[167,348,350,353,355,357,360],{"class":169,"line":349},5,[167,351,352],{"class":300}," rel",[167,354,317],{"class":292},[167,356,320],{"class":292},[167,358,359],{"class":176},"stylesheet",[167,361,362],{"class":292},"\"\n",[167,364,366,369,371,373,376],{"class":169,"line":365},6,[167,367,368],{"class":300}," href",[167,370,317],{"class":292},[167,372,320],{"class":292},[167,374,375],{"class":176},"/assets/style/main.css",[167,377,362],{"class":292},[167,379,381],{"class":169,"line":380},7,[167,382,383],{"class":292}," />\n",[167,385,387,390,392],{"class":169,"line":386},8,[167,388,389],{"class":292}," {\n initSlider()\n})\n","dist/assets/script/main.js","js",[164,552,553,579,585,617,625],{"__ignoreMap":162},[167,554,555,559,562,565,568,571,574,577],{"class":169,"line":108},[167,556,558],{"class":557},"s7zQu","import",[167,560,561],{"class":292}," {",[167,563,564],{"class":409}," initSlider",[167,566,567],{"class":292}," }",[167,569,570],{"class":557}," from",[167,572,573],{"class":292}," \"",[167,575,576],{"class":176},"./modules/slider.js",[167,578,362],{"class":292},[167,580,581],{"class":169,"line":101},[167,582,584],{"emptyLinePlaceholder":583},true,"\n",[167,586,587,590,592,596,599,601,604,606,609,612,615],{"class":169,"line":329},[167,588,589],{"class":409},"document",[167,591,503],{"class":292},[167,593,595],{"class":594},"s2Zo4","addEventListener",[167,597,598],{"class":409},"(",[167,600,320],{"class":292},[167,602,603],{"class":176},"DOMContentLoaded",[167,605,320],{"class":292},[167,607,608],{"class":292},",",[167,610,611],{"class":292}," ()",[167,613,614],{"class":300}," =>",[167,616,509],{"class":292},[167,618,619,622],{"class":169,"line":340},[167,620,621],{"class":594}," initSlider",[167,623,624],{"class":296},"()\n",[167,626,627,630],{"class":169,"line":349},[167,628,629],{"class":292},"}",[167,631,632],{"class":409},")\n",[140,634,636],{"id":635},"как-это-работает-под-капотом","Как это работает под капотом",[638,639,641],"h3",{"id":640},"один-css-файл-без-препроцессоров","Один CSS-файл без препроцессоров",[131,643,644,645,647,648,651],{},"Все стили агрегируются в один ",[164,646,489],{},". Внутри используется нативный CSS с ",[164,649,650],{},"@import"," и встроенным в браузеры нестингом.",[156,653,656],{"className":487,"code":654,"filename":655,"language":490,"meta":162,"style":162},"@import \"./reset.css\";\n@import \"./variables.css\";\n\n.container {\n margin-inline: auto;\n max-width: var(--max-width);\n\n @media (max-width: 640px) {\n padding-inline: 16px;\n }\n}\n","src/assets/styles/main.css",[164,657,658,671,684,688,696,707,724,728,744,756,761],{"__ignoreMap":162},[167,659,660,662,664,667,669],{"class":169,"line":108},[167,661,650],{"class":557},[167,663,573],{"class":292},[167,665,666],{"class":176},"./reset.css",[167,668,320],{"class":292},[167,670,524],{"class":292},[167,672,673,675,677,680,682],{"class":169,"line":101},[167,674,650],{"class":557},[167,676,573],{"class":292},[167,678,679],{"class":176},"./variables.css",[167,681,320],{"class":292},[167,683,524],{"class":292},[167,685,686],{"class":169,"line":329},[167,687,584],{"emptyLinePlaceholder":583},[167,689,690,692,694],{"class":169,"line":340},[167,691,503],{"class":292},[167,693,506],{"class":172},[167,695,509],{"class":292},[167,697,698,701,703,705],{"class":169,"line":349},[167,699,700],{"class":514}," margin-inline",[167,702,150],{"class":292},[167,704,537],{"class":409},[167,706,524],{"class":292},[167,708,709,711,713,716,718,721],{"class":169,"line":365},[167,710,515],{"class":514},[167,712,150],{"class":292},[167,714,715],{"class":594}," var",[167,717,598],{"class":292},[167,719,720],{"class":409},"--max-width",[167,722,723],{"class":292},");\n",[167,725,726],{"class":169,"line":380},[167,727,584],{"emptyLinePlaceholder":583},[167,729,730,733,736,738,741],{"class":169,"line":386},[167,731,732],{"class":409}," @media (",[167,734,735],{"class":514},"max-width",[167,737,150],{"class":292},[167,739,740],{"class":520}," 640px",[167,742,743],{"class":409},") {\n",[167,745,746,749,751,754],{"class":169,"line":396},[167,747,748],{"class":514}," padding-inline",[167,750,150],{"class":292},[167,752,753],{"class":520}," 16px",[167,755,524],{"class":292},[167,757,758],{"class":169,"line":406},[167,759,760],{"class":292}," }\n",[167,762,763],{"class":169,"line":413},[167,764,544],{"class":409},[638,766,768],{"id":767},"стабильные-пути-к-скриптам","Стабильные пути к скриптам",[131,770,771],{},"Vite по умолчанию пытается собрать и забандлить все ассеты. Я обошел это: скрипты не собираются в общий бандл. Лейаут ссылается на файл как обычно:",[156,773,778],{"className":774,"code":775,"filename":776,"language":777,"meta":162,"style":162},"language-astro shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\n","src/layouts/Layout.astro","astro",[164,779,780],{"__ignoreMap":162},[167,781,782],{"class":169,"line":108},[167,783,775],{},[131,785,786,787,790,791,794,795,798],{},"В режиме ",[164,788,789],{},"build"," файлы из ",[164,792,793],{},"src/assets/script/"," просто \"как есть\" копируются в ",[164,796,797],{},"dist/",". Это сохраняет структуру модулей, оставляя код удобным для дальнейшей работы. Никаких хешей и минификации.",[140,800,802],{"id":801},"режимы-сборки","Режимы сборки",[804,805,806,819],"table",{},[807,808,809],"thead",{},[810,811,812,816],"tr",{},[813,814,815],"th",{},"Команда",[813,817,818],{},"Что происходит",[820,821,822,837],"tbody",{},[810,823,824,830],{},[825,826,827],"td",{},[164,828,829],{},"npm run build",[825,831,832,833,836],{},"Сборка ",[135,834,835],{},"для передачи",". CSS/JS не минифицируются, HTML не сжимается. Идеально для отладки и интеграции.",[810,838,839,844],{},[825,840,841],{},[164,842,843],{},"npm run build:prod",[825,845,832,846,849,850,853,854,857],{},[135,847,848],{},"в продакшн",". Всё минифицировано, а ",[164,851,852],{},"main.js"," бандлится через ",[164,855,856],{},"esbuild"," в один компактный файл, импорты разрешаются.",[140,859,861],{"id":860},"итог","Итог",[131,863,864],{},"Этот шаблон во многом заменяет тот самый знакомый многим \"Gulp + PUG/EJS\", объединяя современные технологии для авторинга (Astro, Vite) с максимально предсказуемым и чистым результатом на выходе сборки.",[131,866,867,868,871],{},"Если в проекте HTML/CSS/JS — это ",[135,869,870],{},"конечный продукт",", а не промежуточный артефакт, — шаблон сэкономит вам массу времени.",[873,874,875],"style",{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sqsOY, html code.shiki .sqsOY{--shiki-light:#8796B0;--shiki-default:#B2CCD6;--shiki-dark:#B2CCD6}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}",{"title":162,"searchDepth":101,"depth":101,"links":877},[878,879,880,881,885,886],{"id":142,"depth":101,"text":143},{"id":222,"depth":101,"text":223},{"id":264,"depth":101,"text":265},{"id":635,"depth":101,"text":636,"children":882},[883,884],{"id":640,"depth":329,"text":641},{"id":767,"depth":329,"text":768},{"id":801,"depth":101,"text":802},{"id":860,"depth":101,"text":861},"md",{"icon":54,"githubRepo":55,"githubUrl":56,"publishedAt":57,"tags":889},[59,16,60],{"title":51,"description":52},"guides/templates/astro-clean-template","Y_NWZsk0uByLQ7Zr2av9uwGLAsPeJKw4aYf4NPM-d0w",[894,897,900,903,906,909,912,915],{"title":9,"meta":895,"path":19},{"icon":12,"publishedAt":13,"languageOriginal":14,"tags":896},[16,17,18],{"title":21,"meta":898,"path":29},{"icon":24,"habrUrl":25,"publishedAt":26,"languageOriginal":14,"tags":899},[28,16],{"title":31,"meta":901,"path":38},{"icon":34,"publishedAt":35,"languageOriginal":14,"tags":902},[16,18,37],{"title":40,"meta":904,"path":48},{"icon":24,"habrUrl":43,"publishedAt":44,"languageOriginal":14,"tags":905},[46,47],{"title":51,"meta":907,"path":61},{"icon":54,"githubRepo":55,"githubUrl":56,"publishedAt":57,"tags":908},[59,16,60],{"title":63,"meta":910,"path":74},{"icon":66,"githubRepo":67,"githubUrl":68,"publishedAt":69,"tags":911},[71,72,73,16,60],{"title":76,"meta":913,"path":83},{"icon":79,"githubRepo":80,"githubUrl":81,"publishedAt":69,"tags":914},[71,16],{"title":85,"meta":916,"path":93},{"icon":88,"githubRepo":89,"githubUrl":90,"publishedAt":69,"tags":917},[71,92,16,60],["Reactive",919],{"$scolor-mode":920,"$si18n:cached-locale-configs":923,"$si18n:resolved-locale":162,"$snuxt-seo-utils:routeRules":928,"$stoasts":929,"$ssite-config":930},{"preference":921,"value":921,"unknown":583,"forced":922},"dark",false,{"ru":924,"en":926},{"fallbacks":925,"cacheable":583},[],{"fallbacks":927,"cacheable":583},[],{"head":-1,"seoMeta":-1},[],{"_priority":931,"currentLocale":14,"defaultLocale":14,"env":935,"name":936,"url":937},{"env":932,"url":107,"name":933,"defaultLocale":934,"currentLocale":934},-15,-3,-2,"production","Aganov.dev","https://aganov.dev",["Set"],["ShallowReactive",940],{"sidebar:section-root:content_ru:guides":-1,"changelog-latest-release:content_ru":-1,"sidebar:collection:articles:/docs/guides/articles:content_ru":-1,"sidebar:collection:templates:/docs/guides/templates:content_ru":-1,"content-page:content_ru:/docs/guides/templates/astro-clean-template":-1,"docs-search:ru:":-1,"top-projects:content_ru":-1,"github-stars:davidaganov/davidaganov.github.io":-1,"site-nav:all-sections:content_ru":-1,"docs-flat-nav:content_ru:guides":-1},"/docs/guides/templates/astro-clean-template",["Reactive",943],{}]