lotek 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
lotek/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ """lotek.run - A static site builder."""
2
+
3
+ from importlib.metadata import version, PackageNotFoundError
4
+
5
+ try:
6
+ __version__ = version("lotek-run")
7
+ except PackageNotFoundError:
8
+ __version__ = "unknown"
9
+
10
+ import lotek.cli as cli
11
+
12
+ def main():
13
+ cli.main()
lotek/build.py ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env python3
2
+ """lotek.run -- static site builder
3
+
4
+ Requires: pandoc in PATH or markdown module
5
+ """
6
+
7
+ from lotek.lib.site_config import config
8
+ from lotek.lib.site_time import now_string
9
+ from lotek.lib.pages import generate_pages
10
+ from lotek.lib.posts import generate_posts, load_posts
11
+ from lotek.lib.index import generate_index_landing
12
+ from lotek.lib.dirs import dirs
13
+ from lotek.lib.static import wipe_and_copy_to_output_dir
14
+ from lotek.plugins.rss import generate_rss
15
+ from lotek.plugins.robots import generate_robots
16
+
17
+
18
+ def build():
19
+ """main entry point"""
20
+
21
+ out = dirs.OUTPUT
22
+ print(f"building lotek at {out}")
23
+ out.mkdir(exist_ok=True)
24
+ dirs.OUTPUT_POSTS.mkdir(exist_ok=True)
25
+ dirs.OUTPUT_STATIC.mkdir(exist_ok=True)
26
+ posts = load_posts(dirs.CONTENT_POSTS)
27
+
28
+ generate_posts(posts, out)
29
+ generate_pages(out)
30
+ if config.features.robotstxt:
31
+ print("generating robots.txt...")
32
+ generate_robots(posts, out)
33
+ if config.features.rss:
34
+ print("generating RSS feed...")
35
+ generate_rss(posts, out)
36
+ generate_index_landing(posts, out)
37
+ wipe_and_copy_to_output_dir(out)
38
+
39
+ print(f"built {len(posts)} posts -> output/")
40
+ last_file = out / "_last"
41
+ last_file.write_text(now_string())
lotek/cli.py ADDED
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env python3
2
+ """lotek - operational command for lotek.run."""
3
+ import sys
4
+ import argparse
5
+ from pathlib import Path
6
+ import lotek
7
+
8
+ from lotek.lib.init import init
9
+ from lotek.cmd.add import cmd_add
10
+ from lotek.cmd.build import cmd_build
11
+ from lotek.cmd.clean import cmd_clean
12
+ from lotek.cmd.deploy import cmd_deploy
13
+ from lotek.cmd.list import cmd_list
14
+ from lotek.cmd.publish import cmd_publish, cmd_unpublish
15
+ from lotek.cmd.serve import cmd_serve
16
+
17
+ USAGE = f"""
18
+ lotek - Tiny Blog Management Tool
19
+ ver: {getattr(lotek, "__version__", "unknown")}
20
+
21
+ Build:
22
+ lotek build Build the site
23
+ lotek clean Remove build output
24
+ lotek serve [--port N] Serve output locally (default: 8000)
25
+ lotek deploy Build and deploy via rsync (reads .env)
26
+
27
+ Content:
28
+ lotek init Make a new site from scratch
29
+ lotek list List all posts
30
+ lotek add <title> Create new post
31
+ lotek publish <slug> Mark a post as published
32
+ lotek unpublish <slug> Mark a post as unpublished
33
+ """
34
+
35
+ def setup_cmd_parser():
36
+ parser = argparse.ArgumentParser(prog="lotek")
37
+ subs = parser.add_subparsers(dest="command")
38
+
39
+ i = subs.add_parser("init")
40
+ i.add_argument("path", type=str, default=".", nargs="?")
41
+
42
+ subs.add_parser("build")
43
+
44
+ subs.add_parser("clean")
45
+
46
+ p = subs.add_parser("serve")
47
+ p.add_argument("--port", "-p", type=int, default=8000)
48
+
49
+ p = subs.add_parser("deploy")
50
+ p.add_argument("--skip-build", action="store_true")
51
+
52
+
53
+ subs.add_parser("list")
54
+
55
+ p = subs.add_parser("add")
56
+ p.add_argument("title", nargs="?")
57
+
58
+ p = subs.add_parser("publish")
59
+ p.add_argument("slug")
60
+
61
+ p = subs.add_parser("unpublish")
62
+ p.add_argument("slug")
63
+
64
+ args = parser.parse_args()
65
+ return args
66
+
67
+ def main():
68
+ args = setup_cmd_parser()
69
+ if not args.command:
70
+ print(USAGE)
71
+ return 0
72
+ try:
73
+ if args.command == "init":
74
+ return init(Path.absolute(Path(args.path)))
75
+ if args.command == "build":
76
+ return cmd_build()
77
+ if args.command == "clean":
78
+ return cmd_clean()
79
+ if args.command == "serve":
80
+ return cmd_serve(args.port)
81
+ if args.command == "deploy":
82
+ return cmd_deploy(skip_build=args.skip_build)
83
+ if args.command == "list":
84
+ return cmd_list()
85
+ if args.command == "add":
86
+ return cmd_add(args.title)
87
+ if args.command == "publish":
88
+ return cmd_publish(args.slug)
89
+ if args.command == "unpublish":
90
+ return cmd_unpublish(args.slug)
91
+ except KeyboardInterrupt:
92
+ print("\nInterrupted")
93
+ return 1
94
+ except Exception as e:
95
+ print(f"Error: {e}", file=sys.stderr)
96
+ return 1
97
+
98
+ if __name__ == "__main__":
99
+ main()
lotek/cmd/__init__.py ADDED
File without changes
lotek/cmd/add.py ADDED
@@ -0,0 +1,25 @@
1
+ from datetime import datetime
2
+ from lotek.lib.colors import red, green
3
+ from lotek.lib.dirs import dirs
4
+
5
+ def cmd_add(title):
6
+ if not title:
7
+ print(red("Title required"))
8
+ return 1
9
+ posts_dir = dirs.CONTENT_POSTS
10
+ posts_dir.mkdir(parents=True, exist_ok=True)
11
+ today = datetime.now().strftime("%Y-%m-%d")
12
+ slug = title.lower().replace(" ", "-")
13
+ fname = f"{today}-{slug}.md"
14
+ fp = posts_dir / fname
15
+ if fp.exists():
16
+ print(red(f"Already exists: {fname}"))
17
+ return 1
18
+ template_path = dirs.TEMPLATES / "post.md"
19
+ if not template_path.exists():
20
+ print(red("Templates not found — run 'lotek init' first"))
21
+ return 1
22
+ template = template_path.read_text()
23
+ fp.write_text(template.replace("{title}", title).replace("{date}", today))
24
+ print(green(f"Created: content/posts/{fname}"))
25
+ return 0
lotek/cmd/build.py ADDED
@@ -0,0 +1,12 @@
1
+ import sys
2
+ from lotek.lib.colors import red
3
+ import lotek.build as build_module
4
+
5
+ def cmd_build():
6
+
7
+ try:
8
+ build_module.build()
9
+ return 0
10
+ except Exception as e:
11
+ print(red(f"Build failed: {e}"), file=sys.stderr)
12
+ return 1
lotek/cmd/clean.py ADDED
@@ -0,0 +1,12 @@
1
+
2
+ import shutil
3
+ from lotek.lib.colors import green
4
+ from lotek.lib.dirs import dirs
5
+
6
+ def cmd_clean():
7
+
8
+ output = dirs.OUTPUT
9
+ if output.exists():
10
+ shutil.rmtree(output)
11
+ print(green("Removed output/"))
12
+ return 0
lotek/cmd/deploy.py ADDED
@@ -0,0 +1,52 @@
1
+ import subprocess
2
+
3
+ from lotek.lib.colors import green, red
4
+ from lotek.lib.dirs import dirs
5
+ from lotek.cmd.build import cmd_build
6
+
7
+ def read_env():
8
+ env_path = dirs.CWD / ".env"
9
+ if not env_path.exists():
10
+ return {}
11
+ env = {}
12
+ for line in env_path.read_text().splitlines():
13
+ if "=" in line and not line.startswith("#"):
14
+ k, _, v = line.partition("=")
15
+ env[k.strip()] = v.strip()
16
+ return env
17
+
18
+
19
+ def cmd_deploy(skip_build=False):
20
+ env = read_env()
21
+ user, host, path = (
22
+ env.get("DEPLOY_USER"),
23
+ env.get("DEPLOY_HOST"),
24
+ env.get("DEPLOY_PATH"),
25
+ )
26
+ if not all([user, host, path]):
27
+ print(red("Missing DEPLOY_USER, DEPLOY_HOST, or DEPLOY_PATH in .env"))
28
+ return 1
29
+ if not skip_build:
30
+ print(green("Building..."))
31
+ rc = cmd_build()
32
+ if rc != 0:
33
+ return rc
34
+ dest = f"{user}@{host}:{path}/"
35
+ print(green(f"Deploying to {dest}"))
36
+ result = subprocess.run(
37
+ [
38
+ "rsync",
39
+ "-avz",
40
+ "--exclude=.env",
41
+ "--exclude=*.pyc",
42
+ "--exclude=__pycache__",
43
+ "--exclude=output",
44
+ "output/",
45
+ dest,
46
+ ]
47
+ )
48
+ if result.returncode != 0:
49
+ print(red("Deploy failed"))
50
+ return result.returncode
51
+ print(green("Deployed successfully"))
52
+ return 0
lotek/cmd/list.py ADDED
@@ -0,0 +1,57 @@
1
+ from datetime import datetime
2
+
3
+ from lotek.lib.site_config import config
4
+ from lotek.lib.dirs import dirs
5
+ from lotek.lib.frontmatter import parse_frontmatter
6
+ from lotek.lib.colors import green, BOLD, RESET
7
+
8
+ def _table(headers, rows):
9
+ widths = [len(h) for h in headers]
10
+ for row in rows:
11
+ for i, c in enumerate(row):
12
+ widths[i] = max(widths[i], len(str(c)))
13
+ hdr = "│ " + " │ ".join(h.center(w) for h, w in zip(headers, widths)) + " │"
14
+ print(BOLD + hdr + RESET)
15
+ print("-" * len(hdr))
16
+ for row in rows:
17
+ print("│ " + " │ ".join(str(c).ljust(w) for c, w in zip(row, widths)) + " │")
18
+
19
+ def cmd_list():
20
+ posts_dir = dirs.CONTENT_POSTS
21
+ if not posts_dir.exists():
22
+ print("No posts directory found")
23
+ return 0
24
+ today = datetime.now().date()
25
+ posts = []
26
+ for f in posts_dir.glob("*.md"):
27
+ meta, _ = parse_frontmatter(f.read_text())
28
+ if meta.get("title"):
29
+ posts.append((f, meta))
30
+ if not posts:
31
+ print("No posts found")
32
+ return 0
33
+
34
+ def sort_key(item):
35
+ try:
36
+ return datetime.strptime(item[1].get("date", ""), "%Y-%m-%d").date()
37
+ except ValueError:
38
+ return today
39
+
40
+ posts.sort(key=sort_key, reverse=True)
41
+ rows = []
42
+ for f, meta in posts:
43
+ date_str = meta.get("date", "")
44
+ if meta.get("publish", "").lower() == "false":
45
+ state = "hidden"
46
+ elif config.features.skip_future:
47
+ try:
48
+ d = datetime.strptime(date_str, "%Y-%m-%d").date()
49
+ state = f"in {(d - today).days}d" if d > today else "live"
50
+ except ValueError:
51
+ state = "live"
52
+ else:
53
+ state = "live"
54
+ rows.append([date_str, meta.get("title", ""), f.stem, state])
55
+ print(green(f"{len(posts)} post(s)"))
56
+ _table(["date", "title", "slug", "status"], rows)
57
+ return 0
lotek/cmd/publish.py ADDED
@@ -0,0 +1,63 @@
1
+ import sys
2
+
3
+ from lotek.lib.frontmatter import parse_frontmatter
4
+ from lotek.lib.colors import green, red
5
+ from lotek.lib.dirs import dirs
6
+
7
+ def _strip_datecode(stem):
8
+ if (
9
+ len(stem) > 11
10
+ and stem.startswith("20")
11
+ and stem[4] == stem[7] == "-"
12
+ and stem[10] == "-"
13
+ ):
14
+ return stem[11:]
15
+ return stem
16
+
17
+ def find_post(slug):
18
+ posts_dir = dirs.CONTENT_POSTS
19
+ if not posts_dir.exists():
20
+ return None
21
+ fp = posts_dir / f"{slug}.md"
22
+ if fp.exists():
23
+ return fp
24
+ for f in posts_dir.glob("*.md"):
25
+ if slug == _strip_datecode(f.stem):
26
+ return f
27
+ matches = [
28
+ f for f in posts_dir.glob("*.md") if _strip_datecode(f.stem).startswith(slug)
29
+ ]
30
+ if len(matches) == 1:
31
+ return matches[0]
32
+ if len(matches) > 1:
33
+ print(red(f"Ambiguous: {len(matches)} matches for '{slug}'"))
34
+ for m in matches:
35
+ print(f" {m.stem}")
36
+ sys.exit(2)
37
+ return None
38
+
39
+ def _set_publish(slug, value):
40
+ fp = find_post(slug)
41
+ if not fp:
42
+ print(red(f"Not found: {slug}"))
43
+ return 1
44
+ meta, body = parse_frontmatter(fp.read_text())
45
+ if not meta.get("title"):
46
+ print(red("No title in frontmatter"))
47
+ return 1
48
+ meta["publish"] = value
49
+ fp.write_text(
50
+ "---\n" + "".join(f"{k}: {v}\n" for k, v in meta.items()) + "---\n\n" + body
51
+ )
52
+ print(green(f"{'Published' if value == 'true' else 'Unpublished'}: {slug}"))
53
+ return 0
54
+
55
+
56
+ def cmd_publish(slug):
57
+ """ i don't know if we even need these, really."""
58
+ return _set_publish(slug, "true")
59
+
60
+
61
+ def cmd_unpublish(slug):
62
+ """ i don't know if we even need these, really."""
63
+ return _set_publish(slug, "false")
lotek/cmd/serve.py ADDED
@@ -0,0 +1,18 @@
1
+ import subprocess
2
+ import sys
3
+ from lotek.lib.colors import green, red
4
+ from lotek.lib.dirs import dirs
5
+
6
+ def cmd_serve(port=8000):
7
+ output = dirs.OUTPUT
8
+ if not output.exists():
9
+ print(red("No output/ — run 'lotek build' first."))
10
+ return 1
11
+ print(green(f"Serving at http://localhost:{port} (Ctrl-C to stop)"))
12
+ try:
13
+ subprocess.run(
14
+ [sys.executable, "-m", "http.server", str(port), "-d", str(output)]
15
+ )
16
+ except KeyboardInterrupt:
17
+ pass
18
+ return 0
lotek/lib/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Core library modules for lotek.run."""
lotek/lib/about.py ADDED
@@ -0,0 +1,28 @@
1
+ """generate the about page"""
2
+
3
+ from lotek.lib.site_config import config
4
+ from lotek.lib.dirs import dirs
5
+ from lotek.lib.render import render, render_wrap, md_to_html
6
+ from lotek.lib.frontmatter import parse_frontmatter
7
+
8
+
9
+ def generate_about(out):
10
+ about = dirs.CONTENT_PAGES / "about.md"
11
+ if about.exists():
12
+ meta, body = parse_frontmatter(about.read_text())
13
+ html = md_to_html(body)
14
+ content = render(
15
+ "post.html",
16
+ {
17
+ "TITLE": meta.get("title", "About"),
18
+ "DATE": "",
19
+ "CONTENT": html,
20
+ },
21
+ )
22
+ (out / "about.html").write_text(
23
+ render_wrap(
24
+ content,
25
+ f"About - {config.site.title}",
26
+ url=f"{config.site.url}/about.html",
27
+ )
28
+ )
lotek/lib/colors.py ADDED
@@ -0,0 +1,18 @@
1
+ import os
2
+ import sys
3
+
4
+ BOLD, RESET = "\033[1m", "\033[0m"
5
+
6
+ def _color(t, c):
7
+ if os.isatty(sys.stderr.fileno()):
8
+ return c + t + RESET
9
+ return t
10
+
11
+ def green(t):
12
+ return _color(t, "\033[32m")
13
+
14
+ def red(t):
15
+ return _color(t, "\033[31m")
16
+
17
+ def dim(t):
18
+ return _color(t, "\033[2m")
lotek/lib/dirs.py ADDED
@@ -0,0 +1,26 @@
1
+ """lotek directory structure"""
2
+ from pathlib import Path
3
+
4
+ class Dirs:
5
+ """lotek directory structure"""
6
+ def __init__(self, path=Path.cwd()):
7
+ # pylint: disable=invalid-name
8
+ self.CWD = path
9
+ self.CONTENT = self.CWD / "content"
10
+ self.CONTENT_POSTS = self.CONTENT / "posts"
11
+ self.CONTENT_PAGES = self.CONTENT / "pages"
12
+
13
+ self.STATIC = self.CWD / "static"
14
+ self.IMAGES = self.CWD / "static" / "img"
15
+ self.TEMPLATES = self.CWD / "templates"
16
+
17
+ self.OUTPUT = self.CWD / "output"
18
+ self.OUTPUT_POSTS = self.CWD / "output" / "posts"
19
+ self.OUTPUT_STATIC = self.CWD / "output" / "static"
20
+
21
+ # expected to be buried somewhere in site-packages
22
+ self.PKG = _pkg_path = Path(__file__).parent.parent
23
+ self.PKG_TEMPLATES = self.PKG / "templates"
24
+ self.PKG_STATIC = self.PKG / "static"
25
+
26
+ dirs = Dirs()
@@ -0,0 +1,17 @@
1
+ """frontmatter parsing for markdown files"""
2
+
3
+
4
+ def parse_frontmatter(text):
5
+ """parse the frontmatter from a markdown file, returning a dict of metadata and the body text"""
6
+ if not text.startswith("---\n"):
7
+ return {}, text
8
+ try:
9
+ end = text.index("\n---\n", 4)
10
+ except ValueError:
11
+ return {}, text
12
+ meta = {}
13
+ for line in text[4:end].splitlines():
14
+ if ":" in line:
15
+ k, _, v = line.partition(":")
16
+ meta[k.strip()] = v.strip()
17
+ return meta, text[end + 5 :]
lotek/lib/highlight.py ADDED
@@ -0,0 +1,25 @@
1
+ """Syntax highlighting via Pygments."""
2
+
3
+ import re
4
+ from pygments import highlight
5
+ from pygments.formatters import HtmlFormatter
6
+ from pygments.lexers import get_lexer_by_name
7
+ from pygments.util import ClassNotFound
8
+
9
+ _FENCE = re.compile(r"^```(\w*)\n([\s\S]*?)^```[ \t]*$", re.MULTILINE)
10
+ _formatter = HtmlFormatter(style="default")
11
+
12
+
13
+ def process_code_blocks(text):
14
+ def replace(m):
15
+ lang = m.group(1).strip().lower() or "text"
16
+ code = m.group(2)
17
+ if code.endswith("\n"):
18
+ code = code[:-1]
19
+ try:
20
+ lexer = get_lexer_by_name(lang, stripall=False)
21
+ except ClassNotFound:
22
+ lexer = get_lexer_by_name("text")
23
+ return "\n\n" + highlight(code, lexer, _formatter) + "\n\n"
24
+
25
+ return _FENCE.sub(replace, text)
@@ -0,0 +1,23 @@
1
+ """HTML stubs for rendering templates."""
2
+
3
+ from lotek.lib.site_config import config
4
+
5
+
6
+ def html_stub_index(post):
7
+ """Generate the HTML for a single post in the index page."""
8
+ return f"""
9
+ <li><span class="date">{post["date"]}</span>
10
+ <a href="posts/{post["slug"]}.html">{post["title"]}</a>
11
+ </li>\n
12
+ """
13
+
14
+
15
+ def html_stub_feed_items(post, html):
16
+ """Generate the HTML for a single post in the feed."""
17
+ return f"""
18
+ <item>
19
+ <title>{post['title']}</title>
20
+ <link>{config.site.url}/posts/{post['slug']}.html</link>
21
+ <pubDate>{post['date']}</pubDate>
22
+ <description><![CDATA[{html}]]></description>
23
+ </item>\n"""
lotek/lib/index.py ADDED
@@ -0,0 +1,20 @@
1
+ """index page generator"""
2
+
3
+ from lotek.lib.site_config import config
4
+ from lotek.lib.site_time import now_string
5
+ from lotek.lib.html_stubs import html_stub_index
6
+ from lotek.lib.render import render, render_wrap
7
+
8
+
9
+ def generate_index_landing(posts, out):
10
+ """Generate the index landing page."""
11
+ items = ""
12
+ for post in posts:
13
+ items += html_stub_index(post)
14
+ content = render("index.html", {"ITEMS": items, "DESC": config.site.description})
15
+ (out / "index.html").write_text(
16
+ render_wrap(content, config.site.title, url=config.site.url)
17
+ )
18
+
19
+ last_file = out / "_last"
20
+ last_file.write_text(now_string())
lotek/lib/init.py ADDED
@@ -0,0 +1,107 @@
1
+ """Site initialization command for lotek.run."""
2
+
3
+ import shutil
4
+ from datetime import date
5
+ from pathlib import Path
6
+ from lotek.lib.site_config import DEFAULT_CONFIG_TEMPLATE_PATH
7
+ from lotek.lib.dirs import Dirs
8
+
9
+ # Get the package root directory
10
+ _pkg_path = Path(__file__).parent.parent
11
+
12
+
13
+ def init(site_path: Path) -> None:
14
+ """Initialize a new lotek site in the given directory.
15
+
16
+ Creates the directory structure, copies templates, and generates site-config.toml.
17
+
18
+ Args:
19
+ site_path: Path to the site directory to initialize. Uses cwd if not provided.
20
+ """
21
+ print(f"working path is: {site_path}")
22
+
23
+ dirs: Dirs = Dirs(site_path)
24
+ # Create directory structure
25
+ site_path.mkdir(parents=True, exist_ok=True)
26
+ dirs.CONTENT.mkdir(exist_ok=True)
27
+ dirs.CONTENT_POSTS.mkdir(parents=True, exist_ok=True)
28
+ dirs.CONTENT_PAGES.mkdir(exist_ok=True)
29
+ dirs.STATIC.mkdir(exist_ok=True)
30
+ dirs.IMAGES.mkdir(parents=True, exist_ok=True)
31
+ dirs.TEMPLATES.mkdir(exist_ok=True)
32
+ # Copy templates
33
+ print("Copying templates...")
34
+ for template in dirs.PKG_TEMPLATES.glob("*"):
35
+ dst = dirs.TEMPLATES / template.name
36
+ shutil.copy2(template, dst)
37
+
38
+ # Copy static files
39
+ print("Populating static directory..")
40
+ static_src = dirs.PKG_STATIC
41
+ for item in static_src.iterdir():
42
+ if item.is_dir():
43
+ shutil.copytree(item, dirs.STATIC / item.name, dirs_exist_ok=True)
44
+ else:
45
+ shutil.copy2(item, dirs.STATIC)
46
+
47
+ # Create site-config.toml from defaults
48
+ config_path = site_path / "site-config.toml"
49
+ if not config_path.exists():
50
+ print(f"Creating {config_path} from template...")
51
+ config_path.write_text(DEFAULT_CONFIG_TEMPLATE_PATH.read_text())
52
+ print("✓ Site configuration created")
53
+
54
+ # Create an about page
55
+ about_path = dirs.CONTENT_PAGES / "about.md"
56
+ if not about_path.exists():
57
+ print(f"Creating {about_path} from template...")
58
+ example_about = """---
59
+ title: About
60
+ date: 2026-06-15
61
+ ---
62
+
63
+ lotek is a small static blog generator. It uses very little technology to do this.
64
+
65
+ The name comes from the Lo-Tek in William Gibson's Johnny Mnemonic -- an underground community that lives in the margins of the city, outside the corporate system. Not against technology. Against the assumption that more technology is always better, that the newest version is always correct, that you should replace what works because something newer exists.
66
+
67
+ ---
68
+
69
+ **rss**: /feed.xml
70
+
71
+ ---
72
+
73
+ Built with pandoc and a Python script. No npm. No framework. No build chain.
74
+ """
75
+ about_path.write_text(example_about)
76
+ print("✓ About page created")
77
+
78
+ # Create an example post
79
+ today = date.today().strftime("%Y-%m-%d")
80
+ example_post = dirs.CONTENT_POSTS / f"{today}-welcome.md"
81
+ if not example_post.exists():
82
+ example_content = f"""---
83
+ title: Welcome to Lotek
84
+ date: {today}
85
+ desc: Your first post
86
+ ---
87
+
88
+ Congratulations! You've created a new lotek site.
89
+
90
+ This is your first post. Edit or delete this file to get started.
91
+
92
+ ## Quick Start
93
+
94
+ 1. Add more posts in `content/posts/`
95
+ 2. Add static pages in `content/pages/`
96
+ 3. Customize `site-config.toml` to change site settings
97
+ 4. Run `lotek build` to generate the site
98
+ 5. Serve it with `lotek serve`
99
+ """
100
+ example_post.write_text(example_content)
101
+ print("✓ Example post created")
102
+
103
+ print(f"\nSite initialized at: {dirs.CWD}")
104
+ print("Next steps:")
105
+ print(" - Edit content in content/posts/ and content/pages/")
106
+ print(" - Customize settings in site-config.toml")
107
+ print(" - Run 'lotek build' to generate the site")