diff --git a/.github/workflows/publish-site.yml b/.github/workflows/publish-site.yml
new file mode 100644
index 0000000..40907d4
--- /dev/null
+++ b/.github/workflows/publish-site.yml
@@ -0,0 +1,54 @@
+name: Docs
+
+on:
+ push:
+ branches: ["main"]
+
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: "pages"
+ cancel-in-progress: false
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+
+ - name: Install CuteKit
+ run: pip install git+https://github.com/cute-engineering/cutekit.git@0.7.4
+
+ - name: Setup Pages
+ id: pages
+ uses: actions/configure-pages@v4
+
+ - name: Build with cat
+ run: |
+ pip install -r meta/plugins/requirements.txt
+ python -m cutekit cat build
+
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: .cutekit/build/site
+
+ deploy:
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ runs-on: ubuntu-latest
+ needs: build
+ steps:
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2b6d32c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+.cutekit
+.mypy_cache
diff --git a/license.md b/license.md
new file mode 100644
index 0000000..00040ae
--- /dev/null
+++ b/license.md
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Cute Engineering
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/meta/plugins/__init__.py b/meta/plugins/__init__.py
new file mode 100644
index 0000000..84e9744
--- /dev/null
+++ b/meta/plugins/__init__.py
@@ -0,0 +1,178 @@
+from cutekit import cli, const, shell
+
+import http.server
+import os
+from pathlib import Path
+import markdown
+import dataclasses as dt
+from dataclasses_json import DataClassJsonMixin
+
+from cutekit import const, shell, jexpr
+
+CAT = "ᓚ₍ ^. .^₎"
+DEFAULT_STYLE_PATH = __file__.replace("__init__.py", "default.css")
+
+SITE_DIR = os.path.join(const.META_DIR, "site")
+SITE_BUILD_DIR = os.path.join(const.BUILD_DIR, "site")
+
+
+# MARK: Model ------------------------------------------------------------------
+
+
+def readFile(path: str) -> str:
+ with open(path, "r") as f:
+ return f.read()
+
+
+def writeFile(path: str, content: str):
+ with open(path, "w") as f:
+ f.write(content)
+
+
+def fixupLinks(html: str) -> str:
+ return html.replace('.md"', '.html"').replace(".md#", ".html#")
+
+
+@dt.dataclass
+class Site(DataClassJsonMixin):
+ title: str = dt.field()
+ header: str | None = dt.field(default=None)
+ favicon: str = dt.field(default="🐱")
+ navbar: str = dt.field(default="")
+ footer: str = dt.field(default="")
+
+ path: str = dt.field(default="")
+
+ @staticmethod
+ def load() -> "Site":
+ siteFile = os.path.join(SITE_DIR, "site.json")
+ siteJson = jexpr.include(Path(siteFile))
+ site = Site.from_dict(siteJson)
+ site.path = siteFile
+ return site
+
+ def renderFavicon(self) -> str:
+ svg = f""
+ urlEscape = str.maketrans({"#": "%23", "<": "%3C", ">": "%3E"})
+ svgEscaped = svg.translate(urlEscape)
+ return f"data:image/svg+xml,{svgEscaped}"
+
+ def renderHeader(self) -> str:
+ return f'{self.header or self.title}
'
+
+ def renderAll(self, out: str):
+ styleFile = os.path.join(SITE_DIR, "style.css")
+ if not os.path.exists(styleFile):
+ styleFile = DEFAULT_STYLE_PATH
+
+ style = readFile(styleFile)
+
+ md = markdown.Markdown(extensions=["meta"])
+ files = shell.find(SITE_DIR)
+ for file in files:
+ if file.endswith(".json"):
+ continue
+
+ output = os.path.join(out, file.replace(SITE_DIR + "/", ""))
+ if not file.endswith(".md"):
+ shell.mkdir(os.path.dirname(output))
+ shell.cp(file, out)
+ continue
+
+ output = output.replace(".md", ".html")
+
+ print(f"Rendering {file} -> {output}")
+
+ shell.mkdir(os.path.dirname(output))
+
+ mdContent = readFile(path=file)
+ htmlContent = md.convert(mdContent)
+
+ title = md.Meta.get("title", [""])[0]
+ if title:
+ title = f"{title} - {self.title}"
+ else:
+ title = self.title
+
+ htmlContent = f"""
+
+
+
+
+
+ {title}
+
+
+
+
+
+ {self.renderHeader()}
+
+
+
+ {htmlContent}
+
+
+
+
+"""
+
+ writeFile(output, fixupLinks(htmlContent))
+
+
+# MARK: Public interface -------------------------------------------------------
+
+
+@cli.command(None, "cat", "Tiny site generator")
+def _():
+ print(CAT)
+
+
+@cli.command("b", "cat/build", "Build the site")
+def _() -> None:
+ shell.rmrf(SITE_BUILD_DIR)
+ shell.mkdir(SITE_BUILD_DIR)
+ site = Site.load()
+ site.renderAll(SITE_BUILD_DIR)
+
+
+@cli.command("c", "cat/clean", "Clean the site")
+def _():
+ shell.rmrf(SITE_BUILD_DIR)
+
+
+@cli.command("s", "cat/serve", "Serve the site")
+def _():
+ shell.rmrf(SITE_BUILD_DIR)
+ shell.mkdir(SITE_BUILD_DIR)
+ site = Site.load()
+ site.renderAll(SITE_BUILD_DIR)
+
+ os.chdir(SITE_BUILD_DIR)
+ shell.exec("python3", "-m", "http.server")
+
+
+@cli.command("e", "cat/init", "Initialize the site")
+def _():
+ shell.mkdir(SITE_DIR)
+ writeFile(
+ os.path.join(SITE_DIR, "site.json"),
+ """
+{
+ "favicon": "🐱",
+ "title": "Cat",
+ "header": "ᓚ₍ ^. .^₎",
+ "navbar": "[Home](/)",
+ "footer": "Built with [ᓚ₍ ^. .^₎](https://github.com/cute-engineering/cat)"
+}
+ """,
+ )
+
+ writeFile(
+ os.path.join(SITE_DIR, "index.md"),
+ """
+This is the home page of the site. You can edit this file to change the content of the home page.
+""",
+ )
diff --git a/meta/plugins/__pycache__/__init__.cpython-311.pyc b/meta/plugins/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..a8ea2a3
Binary files /dev/null and b/meta/plugins/__pycache__/__init__.cpython-311.pyc differ
diff --git a/meta/plugins/__pycache__/cat.cpython-311.pyc b/meta/plugins/__pycache__/cat.cpython-311.pyc
new file mode 100644
index 0000000..2be224e
Binary files /dev/null and b/meta/plugins/__pycache__/cat.cpython-311.pyc differ
diff --git a/meta/plugins/default.css b/meta/plugins/default.css
new file mode 100644
index 0000000..ec6a786
--- /dev/null
+++ b/meta/plugins/default.css
@@ -0,0 +1,159 @@
+:root {
+ --width: 720px;
+ --font-main: Verdana, sans-serif;
+ --font-secondary: Verdana, sans-serif;
+ --font-scale: 1em;
+ --background-color: #fff;
+ --heading-color: #222;
+ --text-color: #444;
+ --link-color: #3273dc;
+ --visited-color: #8b6fcb;
+ --code-background-color: #f2f2f2;
+ --code-color: #222;
+ --blockquote-color: #222;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --background-color: #01242e;
+ --heading-color: #eee;
+ --text-color: #ddd;
+ --link-color: #8cc2dd;
+ --visited-color: #8b6fcb;
+ --code-background-color: #000;
+ --code-color: #ddd;
+ --blockquote-color: #ccc;
+ }
+}
+
+body {
+ font-family: var(--font-secondary);
+ font-size: var(--font-scale);
+ margin: auto;
+ padding: 20px;
+ max-width: var(--width);
+ text-align: left;
+ background-color: var(--background-color);
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+ line-height: 1.5;
+ color: var(--text-color);
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ font-family: var(--font-main);
+ color: var(--heading-color);
+}
+
+a {
+ color: var(--link-color);
+ cursor: pointer;
+ text-decoration: none;
+}
+
+a:hover {
+ text-decoration: underline;
+}
+
+nav a {
+ margin-right: 8px;
+}
+
+strong,
+b {
+ color: var(--heading-color);
+}
+
+button {
+ margin: 0;
+ cursor: pointer;
+}
+
+time {
+ font-family: monospace;
+ font-style: normal;
+ font-size: 15px;
+}
+
+main {
+ line-height: 1.6;
+}
+
+table {
+ width: 100%;
+}
+
+hr {
+ border: 0;
+ border-top: 1px dashed;
+}
+
+img {
+ max-width: 100%;
+}
+
+code {
+ font-family: monospace;
+ padding: 2px;
+ background-color: var(--code-background-color);
+ color: var(--code-color);
+ border-radius: 3px;
+}
+
+blockquote {
+ border-left: 1px solid #999;
+ color: var(--code-color);
+ padding-left: 20px;
+ font-style: italic;
+}
+
+footer {
+ padding: 25px 0;
+ text-align: center;
+}
+
+.title:hover {
+ text-decoration: none;
+}
+
+.title h1 {
+ font-size: 1.5em;
+}
+
+.inline {
+ width: auto !important;
+}
+
+.highlight,
+.code {
+ padding: 1px 15px;
+ background-color: var(--code-background-color);
+ color: var(--code-color);
+ border-radius: 3px;
+ margin-block-start: 1em;
+ margin-block-end: 1em;
+ overflow-x: auto;
+}
+
+/* blog post list */
+ul.blog-posts {
+ list-style-type: none;
+ padding: unset;
+}
+
+ul.blog-posts li {
+ display: flex;
+}
+
+ul.blog-posts li span {
+ flex: 0 0 130px;
+}
+
+ul.blog-posts li a:visited {
+ color: var(--visited-color);
+}
diff --git a/meta/plugins/requirements.txt b/meta/plugins/requirements.txt
new file mode 100644
index 0000000..9b2aa88
--- /dev/null
+++ b/meta/plugins/requirements.txt
@@ -0,0 +1 @@
+Markdown~=3.7
diff --git a/meta/site/blog/index.md b/meta/site/blog/index.md
new file mode 100644
index 0000000..2a39f81
--- /dev/null
+++ b/meta/site/blog/index.md
@@ -0,0 +1,2 @@
+# Posts
+ - [Initial Release](initial-release.md)
diff --git a/meta/site/blog/initial-release.md b/meta/site/blog/initial-release.md
new file mode 100644
index 0000000..9c44a0a
--- /dev/null
+++ b/meta/site/blog/initial-release.md
@@ -0,0 +1 @@
+It's cat's first release! 🎉
diff --git a/meta/site/cat.jpg b/meta/site/cat.jpg
new file mode 100644
index 0000000..4d97f66
Binary files /dev/null and b/meta/site/cat.jpg differ
diff --git a/meta/site/index.md b/meta/site/index.md
new file mode 100644
index 0000000..ca5a532
--- /dev/null
+++ b/meta/site/index.md
@@ -0,0 +1,3 @@
+**Cat** is a tiny static site generator for CuteKit.
+
+
diff --git a/meta/site/site.json b/meta/site/site.json
new file mode 100644
index 0000000..10bd41b
--- /dev/null
+++ b/meta/site/site.json
@@ -0,0 +1,7 @@
+{
+ "favicon": "🐱",
+ "title": "Cat",
+ "header": "ᓚ₍ ^. .^₎",
+ "navbar": "[Home](/) [Blog](/blog) [GitHub](https://github.com/cute-engineering/cat)",
+ "footer": "© 2024 Cute Engineering"
+}
diff --git a/project.json b/project.json
new file mode 100644
index 0000000..7566423
--- /dev/null
+++ b/project.json
@@ -0,0 +1,6 @@
+{
+ "$schema": "https://schemas.cute.engineering/stable/cutekit.manifest.project.v1",
+ "id": "cute-engineeering/cat",
+ "type": "project",
+ "description": "A tiny static site generator for CuteKit projects."
+}
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..28604de
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,26 @@
+# 🐱 Cat
+
+A tiny static site generator for CuteKit projects.
+
+## Features
+
+ - Nothing fancy
+ - purrs like a kitten
+ - Easy to use with CuteKit
+
+## Installation
+
+Just add the following piece of json to your `project.json` file:
+
+```json
+{
+ "extern": {
+ "cute-engineering/cat": {
+ "git": "https://github.com/cute-engineering/cat.git",
+ "tag": "v0.7.0"
+ },
+ }
+}
+```
+
+Then run `cutekit model init` to install the package and `cutekit cat init` to initialize the site.