Skip to content

Latest commit

 

History

16 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

tles

A fast, easy blog site builder — write Markdown, preview live, deploy to GitHub Pages.

Rust License OpenSSF Scorecard

tles turns a folder of Markdown files into a complete static blog. It ships a development server with hot reload, a polished theme with dark mode and LLM translations, and a scaffolded GitHub Actions workflow.

Functions

  • Tera templates — tera 2 layouts with built-in helpers for TOC, pagination, Open Graph, tag/category lists and more.
  • Themes — Design your theme.
  • Taxonomies — tag and category pages (plus index pages) are generated automatically.
  • Translations — tles site -t translates every post with an LLM into the languages listed in [i18n] target_lang.
  • Feeds & SEO — atom.xml, sitemap.xml and robots.txt are built in.
  • Rhai helper scripts — extend your templates with sandboxed scripts dropped into helper/.
  • Deploy-ready scaffold — tles site -i writes the config, theme, .gitignore and the GitHub Pages workflow for using.

Quick start

Requires Rust 1.85+ (edition 2024).

# install
cargo install --git https://github.com/iskyrainy/tless

# scaffold a new site
mkdir my-blog && cd my-blog
tles site -i

# write your first post
tles post add hello
tles post publish hello

# preview at http://127.0.0.1:8917
tles server -r

# build the static site into public/
tles site -g

Generated site

tles site -i produces a self-contained, deploy-ready repository:

.github/workflows/deploy.yml   # GitHub Pages deployment
.gitignore
tless.toml                     # site configuration
helper/                        # Rhai helper scripts
plugin/
public/                        # build output (gitignored)
source/
  draft/                       # work-in-progress posts (never built)
  post/                        # published posts
  page/                        # standalone pages
  i18n/<lang>/                 # translations of posts, written by `tles site -t`
  robots.txt
theme/base/
  layout/                      # base.html, index.html, post.html, page.html, tag*.html, category*.html
  assets/                      # style.css, highlight.css, highlight.js, giscus*.css

Names are slugified (First Post -> first-post.md), and every source file is rendered to a pretty URL: source/post/hello.md -> /post/hello/.

Configuration

[site]
title = "My Tless Site"
subtitle = "Notes on what I build, and how."
description = "A fast blog powered by Tless."
rights = "My Tless Site"
author = "Your Name"
url = "http://127.0.0.1:8917"
zone = "Asia/Shanghai"
theme = "base"
favicon = ""
menu = [
    { name = "Home", link = "/index.html" },
    { name = "Tags", link = "/tag" },
    { name = "Categories", link = "/category" }
]

[i18n]                         # optional; see "Translations" below
provider = "deepseek"          # deepseek | kimi | glm
api_key = "..."
model = "deepseek-flash"
target_lang = ["zh-CN", "en"]

[giscus]                       # optional; see "Comments" below
repo = "owner/name"
repo_id = "R_kgDO…"
category = "Announcements"
category_id = "DIC_kwDO…"
mapping = "pathname"
lang = "en"

tless.toml is read at startup and on every rebuild — restart the dev server after editing it.

Templates

Layouts live in theme/<theme>/layout/ and are plain Tera templates. The included base theme extends base.html from each page:

Layout Used for
base.html Shared shell: head, header (nav, language switcher, RSS, dark-mode toggle), footer, back-to-top.
index.html Home feed: the newest post full width, then two per row.
post.html Single post; the default for files in source/post/.
page.html Single page; the default for files in source/page/.
tag.html / category.html One page per term with that term's posts.
tag-index.html / category-index.html Overview of all terms.

A source file can override its layout through frontmatter (layout: post.html).

Every layout gets site, the whole site model. Templates also receive:

Variable On Value
title posts, pages, terms Post/page title; falls back to the source file name.
date posts, pages Frontmatter date, verbatim — feed it to date(ts=…).
content posts, pages Rendered article HTML.
markdown posts, pages Raw Markdown body (what toc() wants).
tag / category posts Lists of terms, or null.
prev_post / next_post posts The post published earlier / later, or null.
translations posts Every published version of this post; see Translations. Empty elsewhere.
name / posts term pages The term and its posts.
recent_posts home All posts, newest first.

prev_post and next_post are { title, date, path, … }; render a link with /post/{{ slugify(str=prev_post.path, is_path=true) }}/.

Built-in helper functions:

Group Functions
Text & URLs date(ts, fmt), url_for(path, relative), full_url_for(path), gravatar(mail), number_format(value, separator), slugify(str, is_path)
HTML tags css, js, link, image, mail, favicon, feed, meta — each takes a path string or an attribute map
Layout partial(name), paginator(current, total, window, base, prev_text, next_text), open_graph(title, description, image, url, type), toc(content, min_level, max_level)
Taxonomies list_category, list_tag, list_post, list_page with orderby, order, amount, list, separator, show_count, tag_class
<article class="post-content">{{ content | safe }}</article>
{{ toc(content=markdown, min_level=2, max_level=3) | safe }}
{{ list_tag(orderby="count", order=-1, amount=10) | safe }}
{{ css(path=["/assets/style.css", "/assets/highlight.css"]) }}
{{ feed(path="/atom.xml", type="application/atom+xml", title=site.config.title) }}

Pass a map instead of a string to set any attribute — keys are written in sorted order, so the output is reproducible:

{{ link(path={"href": "/tag/" ~ t, "class": "rail-tag"}, text=t) }}
{{ js(path={"src": "/assets/highlight.js", "defer": ""}) }}

slugify(str, is_path=true) slugifies the file name of a path, which is what post URLs are built from. date(ts=…) accepts an epoch number, RFC 3339 or the %Y-%m-%d %H:%M:%S the CLI writes.

URLs

Build every link with url_for(path, relative=false). It returns a site-root relative path carrying the sub-path the site is served from, so the same theme works whether [site] url points at a domain root or at a sub-directory:

[site] url url_for(path="/assets/style.css", relative=false)
https://example.com /assets/style.css
https://example.com/blog /blog/assets/style.css

Absolute URLs, //host/…, mailto: and #fragment are returned untouched, so a menu entry pointing at another site keeps working.

Use full_url_for(path) when the URL has to be absolute — feeds and sitemaps, and the giscus theme files, which a third-party iframe fetches.

url_for without relative=false returns ./path, which only resolves on pages at the site root; the base theme never uses it.

Headings get anchor ids while rendering, so the generated table of contents links jump to the right spot.

A simple example for nginx proxy:

location /blog/ {
        add_header Content-Security-Policy upgrade-insecure-requests;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Prefix /blog;
        proxy_pass http://127.0.0.1:8917/;
}

Rhai helpers

Drop a .rhai script into helper/ and it becomes a template function named after the file. Each script must define fn main(args) (call is a reserved keyword in Rhai):

// helper/greeting.rhai → {{ greeting(name="World") }}
fn main(args) {
    "Hello, " + args["name"] + "!"
}

Scripts run sandboxed: no loops, no eval/import, with operation, depth and call-level limits. Adding or editing a script reloads the dev server automatically.

Translations

tles site -t reads every post in source/post/, asks the configured LLM to translate it, and writes the result to source/i18n/<lang>/<name>.md — one file per language in target_lang. A post is only re-translated when its Markdown changed, tracked by an md5 of the source in source/post/.post_hash.json.

[i18n]
provider = "deepseek"          # deepseek | kimi | glm
api_key = "sk-…"
model = "deepseek-flash"
target_lang = ["zh-CN", "en"]  # BCP 47 codes

Important

Supported target_lang code info see more in google language code.

The build (tles site -g) picks the translations up and publishes them next to the original:

source/post/hello.md        →  public/post/hello/index.html
source/i18n/zh-CN/hello.md  →  public/post/zh-CN/hello/index.html
source/i18n/en/hello.md     →  public/post/en/hello/index.html

Post pages get a translations variable listing every version that exists, which is what the base theme's language switcher is built from:

[ { "lang": "",      "url": "/post/hello/",      "current": true  },
  { "lang": "zh-CN", "url": "/post/zh-CN/hello/", "current": false } ]

lang is empty for the original: originals have no fixed language, so nothing labels them but their own path. current marks the page being rendered, so a theme can highlight it.

Code blocks, inline code and frontmatter keys pass through untouched; only title, description, summary and excerpt are translated. Translation prompt see SYSTEM_PROMPT.

Comments

Post pages carry a giscus thread when [giscus] repo is set; leave it empty and the section disappears. Enable Discussions on the repository, install the giscus app, then copy repo_id, category and category_id from the configuration box on giscus.app.

[giscus]
repo = "owner/name"
repo_id = "R_kgDO…"
category = "Announcements"
category_id = "DIC_kwDO…"
mapping = "pathname"           # how a page maps to a discussion
lang = "en"                    # language of the giscus interface

assets/giscus.css for light, assets/giscus-dark.css for dark. The domain https://giscus.app has been allowed by tles.

Theme

The bundled base theme follows the layout of the Cloudflare blog.

Code blocks use the Tokyo Night palette — Tokyo Night Day in light mode, Tokyo Night in dark mode. The token colours are --hl-* custom properties at the top of assets/highlight.css, so retheming highlighting means editing that one block; the block background still comes from --code-bg in style.css.

Deployment

The scaffolded workflow builds the site and publishes public/ to GitHub Pages on every push to main. One-time setup in your repository:

Important

Set Settings → Pages → Source to GitHub Actions, and update url in tless.toml to your final site URL so feeds, sitemap and absolute links are correct.

The workflow installs tles from this repository — point it at your fork if you maintain one.

Development

cargo build --release  # build
cargo test             # unit tests
cargo clippy           # lints
cargo fmt              # formatting

About

A blog application built in rust.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages