Switch to Bascik
From Hugo
Hugo is a static site generator; Bascik is a build tool for HTML components. Both produce vanilla HTML with no client-side framework runtime. The main conceptual shift is that Hugo uses Go template syntax in .html layout files, while Bascik uses vanilla HTML component files composed by tag name. Partials become component files, base template blocks become slot-based layout components, and Go template logic becomes Node.js build scripts.
Mental Model Comparison
| Concept | Hugo | Bascik |
|---|---|---|
| Partial | layouts/partials/nav.html ({{ partial }}) | src/components/site-nav/site-nav.html (<site-nav>) |
| Base template | baseof.html with {{ block }} | <site-layout> with data-bascik-slot |
| Template variables | {{ .Title }}, {{ .Params.x }} | data-bascik-prop-* and data-bascik-attr-* |
| Loops and data | {{ range }}, front matter, data/ | <script data-bascik-build> with Node.js |
| Shortcode | layouts/shortcodes/callout.html | A component tag such as <my-callout> |
| Styles | SCSS through Hugo Pipes | Plain CSS in paired .css files (auto-scoped) |
A Low-Risk First Step
Migrate a simple partial (like a header or footer) into a Bascik component:
- Create a project with
npm create bascik my-site, or add Bascik to an existing folder withnpm install @bascik/bascik, and runnpx bascik init. Bascik's config is an ES module, so the project needs"type": "module"inpackage.json:initadds it, or stops and tells you to add it yourself when the folder already has JavaScript that may rely on CommonJS (see Set up an existing project). - Create
src/components/site-footer/site-footer.html. - Copy the markup from
layouts/partials/footer.htmlinto it. Replace Go template values such as{{ .Site.Title }}with fixed text. - Use
<site-footer />insidesrc/pages/index.html. - Run
npm run devto inspect the generated HTML.
Layouts and Partials → HTML Component Files
Hugo organizes templates under layouts/ with partials in layouts/partials/. In Bascik, every reusable piece of markup is a plain .html file under src/components/, identified by its hyphenated folder name.
Before (Hugo) After (Bascik)
layouts/ src/components/
_default/ site-nav/
baseof.html site-nav.html
single.html site-footer/
list.html site-footer.html
partials/ site-layout/
nav.html site-layout.html
footer.html post-card/
shortcodes/ post-card.html
callout.html src/pages/
content/ index.html
posts/ about.html
my-post.md posts/
static/ my-post.html
css/
main.css {{ partial }} → Bascik Component Tags
A Hugo {{ partial "nav.html" . }} call becomes a self-contained Bascik component tag. The hyphenated folder name is the tag name.
<!-- layouts/partials/nav.html (Hugo - before) -->
<nav class="nav">
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/blog">Blog</a>
</nav>
<!-- Used in a layout with: -->
{{ partial "nav.html" . }} <!-- src/components/site-nav/site-nav.html (Bascik - after) -->
<nav class="nav">
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/blog">Blog</a>
</nav>
<!-- Used in a page with: -->
<site-nav /> Base Templates → Slot-based Layout Components
Hugo's baseof.html with {{ block "main" . }} / {{ define "main" }} maps to a Bascik layout component that uses data-bascik-slot for the page body. Named blocks become named slots.
<!-- layouts/_default/baseof.html (Hugo - before) -->
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ .Title }} - Acme</title>
{{ partial "head-extra.html" . }}
</head>
<body>
{{ partial "nav.html" . }}
<main>{{ block "main" . }}{{ end }}</main>
{{ partial "footer.html" . }}
</body>
</html>
<!-- layouts/_default/single.html (Hugo - before) -->
{{ define "main" }}
<article>
<h1>{{ .Title }}</h1>
{{ .Content }}
</article>
{{ end }} The layout component wraps only the repeated inner structure. The <html>, <head>, and <body> live in the page file; a component that tries to own them fails the build ("Page does not contain a non-empty <body> element"). Shared head tags go in a head component such as <site-head>.
<!-- src/components/site-layout/site-layout.html (Bascik - after) -->
<site-nav />
<main><div data-bascik-slot></div></main>
<site-footer />
<!-- src/pages/posts/my-post.html (Bascik - after) -->
<!DOCTYPE html>
<html lang="en">
<head>
<title>My Post - Acme</title>
</head>
<body>
<site-layout>
<article>
<h1>My Post</h1>
<p>Post body goes here.</p>
</article>
</site-layout>
</body>
</html> Template Variables → data-bascik-prop-* and data-bascik-attr-*
Hugo's {{ .Title }} and {{ .Params.description }} pull values from front matter at build time. In Bascik:
- Use
data-bascik-prop-*to insert text into elements. - Use
data-bascik-attr-{attribute}="{propName}"to bind prop values directly to element attributes (such ashref,src, oralt) while keeping inner text intact.
<!-- layouts/partials/post-card.html (Hugo - before) -->
<div class="post-card">
<h3>{{ .Title }}</h3>
<p>{{ .Params.description }}</p>
<a href="{{ .RelPermalink }}">Read more</a>
</div> <!-- src/components/post-card/post-card.html (Bascik - after) -->
<div class="post-card">
<h3 data-bascik-prop-title></h3>
<p data-bascik-prop-description></p>
<a data-bascik-attr-href="url">Read more</a>
</div>
<!-- Usage -->
<post-card
data-bascik-prop-title="My Post"
data-bascik-prop-description="A short summary."
data-bascik-prop-url="/posts/my-post"
/> Text and attribute values: Bascik props carry plain text strings. For rich HTML content, such as a post body rendered from Markdown, use a slot instead of a prop.
{{ range }} Loops → <script data-bascik-build>
Hugo's {{ range .Pages }} iterates over content at build time. The Bascik equivalent is a <script data-bascik-build> block that reads files with Node.js and returns HTML from its default export.
<!-- layouts/_default/list.html (Hugo - before) -->
{{ define "main" }}
<ul class="post-list">
{{ range .Pages }}
<li>
<a href="{{ .RelPermalink }}">{{ .Title }}</a>
<span>{{ .Date.Format "2006-01-02" }}</span>
</li>
{{ end }}
</ul>
{{ end }} <!-- src/pages/blog.html (Bascik - after) -->
<ul class="post-list">
<script data-bascik-build>
import { readdir, readFile } from "node:fs/promises";
import matter from "gray-matter";
import { html } from "@bascik/bascik/html";
export default async function () {
const files = (await readdir("./content/posts")).filter((f) =>
f.endsWith(".md"),
);
const posts = await Promise.all(
files.map(async (f) => {
const { data } = matter(
await readFile(`./content/posts/${f}`, "utf8"),
);
const slug = f.replace(".md", "");
return { slug, title: data.title, date: new Date(data.date) };
}),
);
posts.sort((a, b) => b.date - a.date);
return html`${posts.map((p) => {
const day = p.date.toISOString().slice(0, 10);
return html`<li>
<a href="/posts/${p.slug}">${p.title}</a><span>${day}</span>
</li>`;
})}`;
}
</script>
</ul> No Hugo data layer: Bascik has no equivalent of Hugo's data directory or content type system. Read Markdown and JSON files directly with Node.js fs and a parser such as gray-matter.
Shortcodes → Components
Hugo shortcodes ({{< callout >}}text{{< /callout >}}) are reusable template fragments. In Bascik, they become component files. For shortcodes that wrap content, use data-bascik-slot.
<!-- layouts/shortcodes/callout.html (Hugo - before) -->
<div class="callout">{{ .Inner }}</div>
<!-- Used in Markdown with: -->
{{< callout >}}This is a tip.{{< /callout >}} <!-- src/components/my-callout/my-callout.html (Bascik - after) -->
<div class="callout">
<div data-bascik-slot></div>
</div>
<!-- Used in a page with: -->
<my-callout><p>This is a tip.</p></my-callout> SCSS / Sass → Plain CSS
Hugo ships a built-in asset pipeline with SCSS/Sass support via css.Sass (the older resources.ToCSS is its alias). Bascik does not include a CSS preprocessor, so write vanilla CSS in the paired .css file alongside each component. Bascik's scoping engine handles selector scoping automatically, so there is rarely a need for nesting or variables beyond what native CSS custom properties and :is() provide.
Before (Hugo) After (Bascik)
assets/
scss/ src/components/
_vars.scss site-nav/
nav.scss site-nav.html
footer.scss site-nav.css ← vanilla CSS, auto-scoped
site-footer/
site-footer.html
site-footer.css Front Matter Data → Build Scripts or Inline HTML
Hugo reads YAML/TOML front matter from content files and makes it available in templates as .Params. In Bascik, individual pages are HTML files, front matter has no direct runtime equivalent. For pages generated from Markdown (a blog, for example), read the front matter with gray-matter inside a <script data-bascik-build> block and inline the values into the HTML.
# content/posts/my-post.md (Hugo - before)
---
title: My Post
description: A short summary.
date: 2026-07-05
---
Post body here. <!-- src/pages/posts/my-post.html (Bascik - after) -->
<script data-bascik-build>
import { readFile } from "node:fs/promises";
import matter from "gray-matter";
import { marked } from "marked";
import { html, raw } from "@bascik/bascik/html";
export default async function () {
const src = await readFile("./content/posts/my-post.md", "utf8");
const { data, content } = matter(src);
return html`
<h1>${data.title}</h1>
<p class="description">${data.description}</p>
${raw(await marked.parse(content))}
`;
}
</script> html escapes the front matter values; raw() marks the Markdown output as HTML to insert as it is, which is safe here because you wrote the Markdown. Sanitize Markdown from anyone else first; see HTML Helpers.