Skip to main content

Astro Clean Template

The main goal of this template is clean, readable, handoff-ready output. Handy for CMS work. No hashed class names, no unreadable script soup, no endless hashes in filenames.

Setup

Scaffold with my Stack tool:

npx @davidaganov/stack

The problem with classic builds

Often a frontend dev does not need a full SPA with heavy logic, but a well-built site (or pages) that will be handed off for CMS integration (Bitrix, WordPress, or custom).

If you reach for the usual stack (Vue/Nuxt/React/Next) for that job, you get a dist folder that is painful to work with:

  • Unpredictable hashes in filenames: main.abc12def.js
  • Bundled styles that are hard to split or override precisely
  • JSX/SFC components you cannot open in the browser as plain HTML

The integrator opens the build and cannot find the right logic because everything is in one minified file. On the other hand, writing everything in raw HTML in 2026 or dragging Gulp into the project is its own kind of pain. You still want components, reusable layouts, and scoped styles.

The solution

Astro Clean Template fixes that. Astro is used for authoring (comfortable components and layouts), but the build is wired so the output is a classic, “old school” file layout.

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

Under the hood

One CSS file, no preprocessors

All styles are aggregated into dist/assets/style/main.css. Native CSS with @import and browser-native nesting.

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;
  }
}

Stable script paths

Vite wants to bundle assets by default. This setup avoids that: scripts are not merged into one app bundle. The layout references the entry as usual:

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

In build mode, files from src/assets/script/ are copied as-is into dist/. Module structure stays intact and the code stays easy to work with. No hashes, no minification.

Build modes

CommandWhat happens
npm run buildHandoff build. CSS/JS are not minified, HTML is not compressed. Great for debugging and integration.
npm run build:prodProduction build. Everything is minified; main.js is bundled with esbuild into one compact file with resolved imports.

Bottom line

This template largely replaces the familiar “Gulp + PUG/EJS” combo: modern authoring (Astro, Vite) with a build output that stays as predictable and clean as possible.

If HTML/CSS/JS is the final product, not an intermediate artifact, this template will save a lot of time.

\n \n\n","html",[164,287,288,305,328,339,348,364,379,385,395,405,412,420,426,441,456,467,476],{"__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,324,326],{"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,323],{"class":176},"en",[167,325,320],{"class":292},[167,327,304],{"class":292},[167,329,331,334,337],{"class":169,"line":330},3,[167,332,333],{"class":292}," <",[167,335,336],{"class":296},"head",[167,338,304],{"class":292},[167,340,342,345],{"class":169,"line":341},4,[167,343,344],{"class":292}," <",[167,346,347],{"class":296},"link\n",[167,349,351,354,356,358,361],{"class":169,"line":350},5,[167,352,353],{"class":300}," rel",[167,355,317],{"class":292},[167,357,320],{"class":292},[167,359,360],{"class":176},"stylesheet",[167,362,363],{"class":292},"\"\n",[167,365,367,370,372,374,377],{"class":169,"line":366},6,[167,368,369],{"class":300}," href",[167,371,317],{"class":292},[167,373,320],{"class":292},[167,375,376],{"class":176},"/assets/style/main.css",[167,378,363],{"class":292},[167,380,382],{"class":169,"line":381},7,[167,383,384],{"class":292}," />\n",[167,386,388,391,393],{"class":169,"line":387},8,[167,389,390],{"class":292}," {\n initSlider()\n})\n","dist/assets/script/main.js","js",[164,554,555,581,587,619,627],{"__ignoreMap":162},[167,556,557,561,564,567,570,573,576,579],{"class":169,"line":108},[167,558,560],{"class":559},"s7zQu","import",[167,562,563],{"class":292}," {",[167,565,566],{"class":410}," initSlider",[167,568,569],{"class":292}," }",[167,571,572],{"class":559}," from",[167,574,575],{"class":292}," \"",[167,577,578],{"class":176},"./modules/slider.js",[167,580,363],{"class":292},[167,582,583],{"class":169,"line":101},[167,584,586],{"emptyLinePlaceholder":585},true,"\n",[167,588,589,592,594,598,601,603,606,608,611,614,617],{"class":169,"line":330},[167,590,591],{"class":410},"document",[167,593,504],{"class":292},[167,595,597],{"class":596},"s2Zo4","addEventListener",[167,599,600],{"class":410},"(",[167,602,320],{"class":292},[167,604,605],{"class":176},"DOMContentLoaded",[167,607,320],{"class":292},[167,609,610],{"class":292},",",[167,612,613],{"class":292}," ()",[167,615,616],{"class":300}," =>",[167,618,510],{"class":292},[167,620,621,624],{"class":169,"line":341},[167,622,623],{"class":596}," initSlider",[167,625,626],{"class":296},"()\n",[167,628,629,632],{"class":169,"line":350},[167,630,631],{"class":292},"}",[167,633,634],{"class":410},")\n",[140,636,638],{"id":637},"under-the-hood","Under the hood",[640,641,643],"h3",{"id":642},"one-css-file-no-preprocessors","One CSS file, no preprocessors",[131,645,646,647,649,650,653],{},"All styles are aggregated into ",[164,648,490],{},". Native CSS with ",[164,651,652],{},"@import"," and browser-native nesting.",[156,655,658],{"className":488,"code":656,"filename":657,"language":491,"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,659,660,673,686,690,698,709,726,730,746,758,763],{"__ignoreMap":162},[167,661,662,664,666,669,671],{"class":169,"line":108},[167,663,652],{"class":559},[167,665,575],{"class":292},[167,667,668],{"class":176},"./reset.css",[167,670,320],{"class":292},[167,672,526],{"class":292},[167,674,675,677,679,682,684],{"class":169,"line":101},[167,676,652],{"class":559},[167,678,575],{"class":292},[167,680,681],{"class":176},"./variables.css",[167,683,320],{"class":292},[167,685,526],{"class":292},[167,687,688],{"class":169,"line":330},[167,689,586],{"emptyLinePlaceholder":585},[167,691,692,694,696],{"class":169,"line":341},[167,693,504],{"class":292},[167,695,507],{"class":172},[167,697,510],{"class":292},[167,699,700,703,705,707],{"class":169,"line":350},[167,701,702],{"class":515}," margin-inline",[167,704,519],{"class":292},[167,706,539],{"class":410},[167,708,526],{"class":292},[167,710,711,713,715,718,720,723],{"class":169,"line":366},[167,712,516],{"class":515},[167,714,519],{"class":292},[167,716,717],{"class":596}," var",[167,719,600],{"class":292},[167,721,722],{"class":410},"--max-width",[167,724,725],{"class":292},");\n",[167,727,728],{"class":169,"line":381},[167,729,586],{"emptyLinePlaceholder":585},[167,731,732,735,738,740,743],{"class":169,"line":387},[167,733,734],{"class":410}," @media (",[167,736,737],{"class":515},"max-width",[167,739,519],{"class":292},[167,741,742],{"class":522}," 640px",[167,744,745],{"class":410},") {\n",[167,747,748,751,753,756],{"class":169,"line":397},[167,749,750],{"class":515}," padding-inline",[167,752,519],{"class":292},[167,754,755],{"class":522}," 16px",[167,757,526],{"class":292},[167,759,760],{"class":169,"line":407},[167,761,762],{"class":292}," }\n",[167,764,765],{"class":169,"line":414},[167,766,546],{"class":410},[640,768,770],{"id":769},"stable-script-paths","Stable script paths",[131,772,773],{},"Vite wants to bundle assets by default. This setup avoids that: scripts are not merged into one app bundle. The layout references the entry as usual:",[156,775,780],{"className":776,"code":777,"filename":778,"language":779,"meta":162,"style":162},"language-astro shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\n","src/layouts/Layout.astro","astro",[164,781,782],{"__ignoreMap":162},[167,783,784],{"class":169,"line":108},[167,785,777],{},[131,787,788,789,792,793,796,797,800,801,804],{},"In ",[164,790,791],{},"build"," mode, files from ",[164,794,795],{},"src/assets/script/"," are copied ",[135,798,799],{},"as-is"," into ",[164,802,803],{},"dist/",". Module structure stays intact and the code stays easy to work with. No hashes, no minification.",[140,806,808],{"id":807},"build-modes","Build modes",[810,811,812,825],"table",{},[813,814,815],"thead",{},[816,817,818,822],"tr",{},[819,820,821],"th",{},"Command",[819,823,824],{},"What happens",[826,827,828,842],"tbody",{},[816,829,830,836],{},[831,832,833],"td",{},[164,834,835],{},"npm run build",[831,837,838,841],{},[135,839,840],{},"Handoff"," build. CSS/JS are not minified, HTML is not compressed. Great for debugging and integration.",[816,843,844,849],{},[831,845,846],{},[164,847,848],{},"npm run build:prod",[831,850,851,854,855,858,859,862],{},[135,852,853],{},"Production"," build. Everything is minified; ",[164,856,857],{},"main.js"," is bundled with ",[164,860,861],{},"esbuild"," into one compact file with resolved imports.",[140,864,866],{"id":865},"bottom-line","Bottom line",[131,868,869],{},"This template largely replaces the familiar “Gulp + PUG/EJS” combo: modern authoring (Astro, Vite) with a build output that stays as predictable and clean as possible.",[131,871,872,873,876],{},"If HTML/CSS/JS is the ",[135,874,875],{},"final product",", not an intermediate artifact, this template will save a lot of time.",[878,879,880],"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":882},[883,884,885,886,890,891],{"id":142,"depth":101,"text":143},{"id":222,"depth":101,"text":223},{"id":264,"depth":101,"text":265},{"id":637,"depth":101,"text":638,"children":887},[888,889],{"id":642,"depth":330,"text":643},{"id":769,"depth":330,"text":770},{"id":807,"depth":101,"text":808},{"id":865,"depth":101,"text":866},"md",{"icon":54,"githubRepo":55,"githubUrl":56,"publishedAt":57,"tags":894},[59,16,60],{"title":51,"description":52},"guides/templates/astro-clean-template","F6JST-KWA6A1yDIdweZw-F-CMvoVRkzyZtXEfBec_nk",[899,902,905,908,911,914,917,920],{"title":9,"meta":900,"path":19},{"icon":12,"publishedAt":13,"languageOriginal":14,"tags":901},[16,17,18],{"title":21,"meta":903,"path":29},{"icon":24,"habrUrl":25,"publishedAt":26,"languageOriginal":14,"tags":904},[28,16],{"title":31,"meta":906,"path":38},{"icon":34,"publishedAt":35,"languageOriginal":14,"tags":907},[16,18,37],{"title":40,"meta":909,"path":48},{"icon":24,"habrUrl":43,"publishedAt":44,"languageOriginal":14,"tags":910},[46,47],{"title":51,"meta":912,"path":61},{"icon":54,"githubRepo":55,"githubUrl":56,"publishedAt":57,"tags":913},[59,16,60],{"title":63,"meta":915,"path":74},{"icon":66,"githubRepo":67,"githubUrl":68,"publishedAt":69,"tags":916},[71,72,73,16,60],{"title":76,"meta":918,"path":83},{"icon":79,"githubRepo":80,"githubUrl":81,"publishedAt":69,"tags":919},[71,16],{"title":85,"meta":921,"path":93},{"icon":88,"githubRepo":89,"githubUrl":90,"publishedAt":69,"tags":922},[71,92,16,60],["Reactive",924],{"$scolor-mode":925,"$si18n:cached-locale-configs":928,"$si18n:resolved-locale":323,"$snuxt-seo-utils:routeRules":933,"$stoasts":934,"$ssite-config":935},{"preference":926,"value":926,"unknown":585,"forced":927},"dark",false,{"ru":929,"en":931},{"fallbacks":930,"cacheable":585},[],{"fallbacks":932,"cacheable":585},[],{"head":-1,"seoMeta":-1},[],{"_priority":936,"currentLocale":14,"defaultLocale":14,"env":940,"name":941,"url":942},{"env":937,"url":107,"name":938,"defaultLocale":939,"currentLocale":939},-15,-3,-2,"production","Aganov.dev","https://aganov.dev",["Set"],["ShallowReactive",945],{"sidebar:section-root:content_en:guides":-1,"changelog-latest-release:content_en":-1,"sidebar:collection:articles:/docs/guides/articles:content_en":-1,"sidebar:collection:templates:/docs/guides/templates:content_en":-1,"content-page:content_en:/docs/guides/templates/astro-clean-template":-1,"docs-search:en:":-1,"top-projects:content_en":-1,"github-stars:davidaganov/davidaganov.github.io":-1,"site-nav:all-sections:content_en":-1,"docs-flat-nav:content_en:guides":-1},"/en/docs/guides/templates/astro-clean-template",["Reactive",948],{}]