Compare commits
3 commits
bc21240e9f
...
550ce3dabc
| Author | SHA1 | Date | |
|---|---|---|---|
| 550ce3dabc | |||
| f3d6366c83 | |||
| c1ad1593b6 |
7 changed files with 296 additions and 62 deletions
34
index.ts
34
index.ts
|
|
@ -1,18 +1,19 @@
|
||||||
import { readdir } from "node:fs/promises";
|
import { readdir } from "node:fs/promises";
|
||||||
import { YAML } from "bun";
|
import { YAML } from "bun";
|
||||||
import metadata from "./metadata.json";
|
import metadata from "./metadata.json";
|
||||||
|
import templateRaw from "./templates/post.html" with { type: "text" };
|
||||||
const templateRaw = await Bun.file("./template.html").text();
|
const templateStr = templateRaw as unknown as string;
|
||||||
const template = templateRaw
|
const template = templateStr
|
||||||
.replace("[[NAME]]", metadata.name)
|
.replaceAll("[[SITE]]", metadata.title)
|
||||||
.replace("[[YEAR]]", new Date().getFullYear().toString());
|
.replaceAll("[[NAME]]", metadata.name)
|
||||||
|
.replaceAll("[[TIMEZONE]]", metadata.timezone)
|
||||||
|
.replaceAll("[[YEAR]]", new Date().getFullYear().toString());
|
||||||
const posts = await readdir("./posts");
|
const posts = await readdir("./posts");
|
||||||
|
|
||||||
const defaultFrontmatter = {
|
const defaultFrontmatter = {
|
||||||
thumb: "",
|
|
||||||
title: "Untitled",
|
title: "Untitled",
|
||||||
desc: "",
|
desc: "",
|
||||||
date: "00-00-0000", // DD-MM-YYYY
|
date: "0000-00-00", // YYYY-MM-DD
|
||||||
tags: [] as never[],
|
tags: [] as never[],
|
||||||
draft: true,
|
draft: true,
|
||||||
};
|
};
|
||||||
|
|
@ -20,10 +21,11 @@ const defaultFrontmatter = {
|
||||||
function fetchFrontmatter(raw?: string) {
|
function fetchFrontmatter(raw?: string) {
|
||||||
try {
|
try {
|
||||||
if (!raw) throw new Error("No frontmatter found.");
|
if (!raw) throw new Error("No frontmatter found.");
|
||||||
return { ...defaultFrontmatter, ...(YAML.parse(raw) as typeof defaultFrontmatter) };
|
const parsed = { ...defaultFrontmatter, ...(YAML.parse(raw) as typeof defaultFrontmatter) };
|
||||||
|
return { ...parsed, date: new Date(parsed.date) };
|
||||||
} catch {
|
} catch {
|
||||||
console.warn("Failed to parse frontmatter. Using default values.");
|
console.warn("Failed to parse frontmatter. Using default values.");
|
||||||
return { ...defaultFrontmatter };
|
return { ...defaultFrontmatter, date: new Date(NaN) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -36,24 +38,18 @@ posts.forEach(async (post) => {
|
||||||
const frontmatter = fetchFrontmatter(raw.at(0));
|
const frontmatter = fetchFrontmatter(raw.at(0));
|
||||||
|
|
||||||
const html = Bun.markdown.html(content, {
|
const html = Bun.markdown.html(content, {
|
||||||
tables: true,
|
|
||||||
strikethrough: true,
|
|
||||||
tasklists: true,
|
|
||||||
autolinks: true,
|
autolinks: true,
|
||||||
headings: true,
|
headings: true,
|
||||||
hardSoftBreaks: true,
|
|
||||||
wikiLinks: true,
|
|
||||||
underline: true,
|
underline: true,
|
||||||
latexMath: true,
|
latexMath: true,
|
||||||
collapseWhitespace: true,
|
|
||||||
permissiveAtxHeaders: true,
|
|
||||||
noIndentedCodeBlocks: true,
|
|
||||||
noHtmlBlocks: true,
|
|
||||||
noHtmlSpans: true,
|
|
||||||
tagFilter: true,
|
tagFilter: true,
|
||||||
});
|
});
|
||||||
const render = template
|
const render = template
|
||||||
.replaceAll("[[TITLE]]", `${metadata.title} - ${frontmatter.title}`)
|
.replaceAll("[[TITLE]]", `${metadata.title} - ${frontmatter.title}`)
|
||||||
|
.replaceAll("[[POST_TITLE]]", frontmatter.title)
|
||||||
|
.replaceAll("[[DATE]]", frontmatter.date.toDateString())
|
||||||
|
.replaceAll("[[REVISIONS]]", `https://git.satr14.my.id/satr14/ssg.md/commits/branch/main/posts/${post}`)
|
||||||
|
.replaceAll("[[DESC]]", frontmatter.desc)
|
||||||
.replace("[[CONTENT]]", html);
|
.replace("[[CONTENT]]", html);
|
||||||
|
|
||||||
await Bun.write(`./dist/posts/${post.replace(".md", ".html")}`, render);
|
await Bun.write(`./dist/posts/${post.replace(".md", ".html")}`, render);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,11 @@
|
||||||
"module": "index.ts",
|
"module": "index.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"tw-dev": "bunx tailwindcss -i style.css -o dist/style.css --watch",
|
||||||
|
"prev": "bunx serve dist",
|
||||||
|
"bun-dev": "bun --watch index.ts"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest"
|
"@types/bun": "latest"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
106
posts/test.md
106
posts/test.md
|
|
@ -1,9 +1,8 @@
|
||||||
thumb: https://raw.githubusercontent.com/satr14/ssg/master/assets/markdown.png
|
|
||||||
title: Comprehensive Markdown & GFM Syntax Reference
|
title: Comprehensive Markdown & GFM Syntax Reference
|
||||||
desc: A detailed guide to Markdown and GitHub Flavored Markdown (GFM) syntax, covering text formatting, lists, tables, code blocks, links, and footnotes.
|
desc: A detailed guide to Markdown and GitHub Flavored Markdown (GFM) syntax, covering text formatting, lists, tables, code blocks, links, and footnotes.
|
||||||
date: 25-05-2026
|
date: 2026-05-27
|
||||||
tags: [markdown, gfm, syntax, reference, guide]
|
tags: [markdown, gfm, syntax, reference, guide]
|
||||||
draft: false
|
draft: true
|
||||||
^---
|
^---
|
||||||
|
|
||||||
## 1. Text Formatting & Structure
|
## 1. Text Formatting & Structure
|
||||||
|
|
@ -57,18 +56,93 @@ draft: false
|
||||||
|
|
||||||
## 4. Code Blocks with Syntax Highlighting
|
## 4. Code Blocks with Syntax Highlighting
|
||||||
|
|
||||||
```nix
|
```bash
|
||||||
{ config, pkgs, ... }:
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
{
|
# Variables & string manipulation
|
||||||
environment.systemPackages = with pkgs; [
|
NAME="world"
|
||||||
git
|
GREETING="Hello, ${NAME}!"
|
||||||
neovim
|
UPPER=${GREETING^^}
|
||||||
tmux
|
SLICE=${GREETING:0:5}
|
||||||
];
|
echo "$UPPER | $SLICE"
|
||||||
|
|
||||||
nix.settings.experimental-features = [ "nix-command" "flakes" ];
|
# Arrays
|
||||||
|
FRUITS=(apple banana cherry)
|
||||||
|
FRUITS+=(mango)
|
||||||
|
echo "${FRUITS[@]}" # all elements
|
||||||
|
echo "${#FRUITS[@]}" # length
|
||||||
|
echo "${FRUITS[@]:1:2}" # slice
|
||||||
|
|
||||||
|
# Associative array
|
||||||
|
declare -A CONFIG=([host]="localhost" [port]="8080")
|
||||||
|
echo "${CONFIG[host]}:${CONFIG[port]}"
|
||||||
|
|
||||||
|
# Arithmetic
|
||||||
|
COUNT=0
|
||||||
|
(( COUNT += 5 ))
|
||||||
|
RESULT=$(( COUNT ** 2 ))
|
||||||
|
echo "$RESULT"
|
||||||
|
|
||||||
|
# Conditionals & test operators
|
||||||
|
FILE="/etc/hosts"
|
||||||
|
if [[ -f "$FILE" && -r "$FILE" ]]; then
|
||||||
|
echo "readable"
|
||||||
|
elif [[ -e "$FILE" ]]; then
|
||||||
|
echo "exists but not readable"
|
||||||
|
else
|
||||||
|
echo "not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Case
|
||||||
|
case "$NAME" in
|
||||||
|
world|earth) echo "planet" ;;
|
||||||
|
*) echo "unknown" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Loops
|
||||||
|
for fruit in "${FRUITS[@]}"; do
|
||||||
|
echo "fruit: $fruit"
|
||||||
|
done
|
||||||
|
|
||||||
|
for (( i=0; i<3; i++ )); do
|
||||||
|
echo "i=$i"
|
||||||
|
done
|
||||||
|
|
||||||
|
WHILE=3
|
||||||
|
while (( WHILE-- > 0 )); do
|
||||||
|
echo "while: $WHILE"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Functions with local vars & return codes
|
||||||
|
greet() {
|
||||||
|
local name=${1:-"stranger"}
|
||||||
|
local -r greeting="Hi, $name!"
|
||||||
|
echo "$greeting"
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
greet "$NAME"
|
||||||
|
|
||||||
|
# Command substitution & process substitution
|
||||||
|
DATE=$(date +%Y-%m-%d)
|
||||||
|
DIFF=<(echo "foo")
|
||||||
|
echo "Today: $DATE"
|
||||||
|
|
||||||
|
# Here-doc
|
||||||
|
cat <<EOF
|
||||||
|
Multiline
|
||||||
|
text block
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Pipelines, redirection & traps
|
||||||
|
trap 'echo "exiting"; exit 1' ERR
|
||||||
|
echo "hello" | tr '[:lower:]' '[:upper:]' > /dev/null 2>&1
|
||||||
|
|
||||||
|
# Parameter expansion: default, error, replace
|
||||||
|
echo "${UNSET:-default}"
|
||||||
|
echo "${GREETING/Hello/Hey}"
|
||||||
|
echo "${FILE##*/}" # basename
|
||||||
|
echo "${FILE%/*}" # dirname
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -78,11 +152,3 @@ draft: false
|
||||||
* **Standard Link:** [Search Engine](https://www.google.com)
|
* **Standard Link:** [Search Engine](https://www.google.com)
|
||||||
* **Link with Tooltip:** [GitHub](https://github.com "Go to GitHub")
|
* **Link with Tooltip:** [GitHub](https://github.com "Go to GitHub")
|
||||||
* **Autolink (GFM):** https://nixos.org
|
* **Autolink (GFM):** https://nixos.org
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Footnotes (GFM)
|
|
||||||
|
|
||||||
You can reference a footnote at the end of a sentence[^1].
|
|
||||||
|
|
||||||
[^1]: This is the text of the footnote, which typically renders at the bottom of the document.
|
|
||||||
103
style.css
103
style.css
|
|
@ -1,4 +1,105 @@
|
||||||
|
@import url("https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400..700;1,400..700&display=swap");
|
||||||
|
|
||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "@catppuccin/tailwindcss/mocha.css";
|
@import "@catppuccin/tailwindcss/mocha.css";
|
||||||
|
|
||||||
body { @apply bg-ctp-base text-ctp-text; }
|
@theme { --font-custom: "Lora", serif; }
|
||||||
|
body { @apply bg-ctp-crust text-ctp-text; }
|
||||||
|
|
||||||
|
h1 { @apply text-3xl; }
|
||||||
|
h2 { @apply text-2xl; }
|
||||||
|
h3 { @apply text-xl; }
|
||||||
|
h4 { @apply text-lg; }
|
||||||
|
h5 { @apply text-base; }
|
||||||
|
h6 { @apply text-sm; }
|
||||||
|
h1, h2, h3, h4, h5, h6 { @apply font-semibold my-2; }
|
||||||
|
h1 { @apply text-ctp-blue; }
|
||||||
|
h2, h3, h4, h5, h6 { @apply text-ctp-lavender; }
|
||||||
|
a:not(h1 a, h2 a, h3 a, h4 a, h5 a, h6 a) { @apply decoration-ctp-blue underline hover:no-underline; }
|
||||||
|
|
||||||
|
p { @apply text-base; }
|
||||||
|
hr { @apply border-t border-ctp-surface1 my-6; }
|
||||||
|
|
||||||
|
ul, ol { @apply list-inside my-4 pl-2; }
|
||||||
|
ol { @apply list-decimal; }
|
||||||
|
ul { @apply list-disc; }
|
||||||
|
ul ul, ul ol, ol ul, ol ol { @apply my-0 pl-6; }
|
||||||
|
input[type="checkbox"] { @apply mr-2 align-middle cursor-default accent-ctp-blue; }
|
||||||
|
|
||||||
|
table { @apply block border-collapse overflow-x-auto my-4; }
|
||||||
|
table th, table td { @apply border border-ctp-surface1 px-4 py-2; }
|
||||||
|
table th { @apply bg-ctp-surface0 font-semibold; }
|
||||||
|
|
||||||
|
blockquote { @apply border-l-4 border-ctp-surface1 pl-4 italic my-4; }
|
||||||
|
code { @apply text-ctp-text rounded px-1; }
|
||||||
|
pre { @apply bg-ctp-base text-ctp-text rounded p-4 overflow-x-auto; }
|
||||||
|
|
||||||
|
/*pre code.hljs {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.hljs-comment {
|
||||||
|
color: #697070;
|
||||||
|
}
|
||||||
|
.hljs-punctuation,
|
||||||
|
.hljs-tag {
|
||||||
|
color: #444a;
|
||||||
|
}
|
||||||
|
.hljs-tag .hljs-attr,
|
||||||
|
.hljs-tag .hljs-name {
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
.hljs-attribute,
|
||||||
|
.hljs-doctag,
|
||||||
|
.hljs-keyword,
|
||||||
|
.hljs-meta .hljs-keyword,
|
||||||
|
.hljs-name,
|
||||||
|
.hljs-selector-tag {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.hljs-deletion,
|
||||||
|
.hljs-number,
|
||||||
|
.hljs-quote,
|
||||||
|
.hljs-selector-class,
|
||||||
|
.hljs-selector-id,
|
||||||
|
.hljs-string,
|
||||||
|
.hljs-template-tag,
|
||||||
|
.hljs-type {
|
||||||
|
color: #800;
|
||||||
|
}
|
||||||
|
.hljs-section,
|
||||||
|
.hljs-title {
|
||||||
|
color: #800;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.hljs-link,
|
||||||
|
.hljs-operator,
|
||||||
|
.hljs-regexp,
|
||||||
|
.hljs-selector-attr,
|
||||||
|
.hljs-selector-pseudo,
|
||||||
|
.hljs-symbol,
|
||||||
|
.hljs-template-variable,
|
||||||
|
.hljs-variable {
|
||||||
|
color: #ab5656;
|
||||||
|
}
|
||||||
|
.hljs-literal {
|
||||||
|
color: #695;
|
||||||
|
}
|
||||||
|
.hljs-addition,
|
||||||
|
.hljs-built_in,
|
||||||
|
.hljs-bullet,
|
||||||
|
.hljs-code {
|
||||||
|
color: #397300;
|
||||||
|
}
|
||||||
|
.hljs-meta {
|
||||||
|
color: #1f7199;
|
||||||
|
}
|
||||||
|
.hljs-meta .hljs-string {
|
||||||
|
color: #38a;
|
||||||
|
}
|
||||||
|
.hljs-emphasis {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.hljs-strong {
|
||||||
|
font-weight: 700;
|
||||||
|
}*/
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>[[TITLE]]</title>
|
|
||||||
<link rel="stylesheet" href="../style.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1>[[TITLE]]</h1>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
[[CONTENT]]
|
|
||||||
</main>
|
|
||||||
<footer>
|
|
||||||
<p>© [[YEAR]] [[NAME]]. All rights reserved.</p>
|
|
||||||
</footer>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
43
templates/home.html
Normal file
43
templates/home.html
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>[[TITLE]]</title>
|
||||||
|
<link rel="stylesheet" href="../style.css">
|
||||||
|
|
||||||
|
<meta name="description" content="[[DESC]]">
|
||||||
|
<meta name="author" content="[[NAME]]">
|
||||||
|
|
||||||
|
<meta property="og:type" content="article">
|
||||||
|
<meta property="og:title" content="[[TITLE]]">
|
||||||
|
<meta property="og:description" content="[[DESC]]">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="//unpkg.com/@catppuccin/highlightjs@1.0.1/css/catppuccin-mocha.css">
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
|
||||||
|
<script>
|
||||||
|
setInterval(() => {
|
||||||
|
const now = new Date();
|
||||||
|
document.querySelector('#localtime').textContent = now.toLocaleTimeString("en-US", { timeZone: "[[TIMEZONE]]", hour: "numeric", minute: "2-digit" });
|
||||||
|
}, 1000);
|
||||||
|
hljs.highlightAll();
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body class="max-w-4xl mx-auto p-4 font-custom">
|
||||||
|
<header class="py-4 flex gap-2 justify-between">
|
||||||
|
<a href="/" class="font-bold text-base font-mono">[[SITE]]</a>
|
||||||
|
<div class="group flex gap-2 font-mono text-ctp-subtext1">
|
||||||
|
<span class="hidden group-hover:block">[[TIMEZONE]]</span>
|
||||||
|
<span id="localtime" class="block group-hover:hidden">--:-- --</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main class="my-6">
|
||||||
|
<h1 class="text-4xl font-bold">[[POST_TITLE]]</h1>
|
||||||
|
<span class="text-ctp-subtext0 italic block mb-10">[[DATE]] - <a href="[[REVISIONS]]">view revisions</a></span>
|
||||||
|
[[CONTENT]]
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
<p class="text-center italic text-shadow-ctp-subtext1">© [[YEAR]] [[NAME]]. All rights reserved.</p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
43
templates/post.html
Normal file
43
templates/post.html
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>[[TITLE]]</title>
|
||||||
|
<link rel="stylesheet" href="../style.css">
|
||||||
|
|
||||||
|
<meta name="description" content="[[DESC]]">
|
||||||
|
<meta name="author" content="[[NAME]]">
|
||||||
|
|
||||||
|
<meta property="og:type" content="article">
|
||||||
|
<meta property="og:title" content="[[TITLE]]">
|
||||||
|
<meta property="og:description" content="[[DESC]]">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="//unpkg.com/@catppuccin/highlightjs@1.0.1/css/catppuccin-mocha.css">
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
|
||||||
|
<script>
|
||||||
|
setInterval(() => {
|
||||||
|
const now = new Date();
|
||||||
|
document.querySelector('#localtime').textContent = now.toLocaleTimeString("en-US", { timeZone: "[[TIMEZONE]]", hour: "numeric", minute: "2-digit" });
|
||||||
|
}, 1000);
|
||||||
|
hljs.highlightAll();
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body class="max-w-4xl mx-auto p-4 font-custom">
|
||||||
|
<header class="py-4 flex gap-2 justify-between">
|
||||||
|
<a href="/" class="font-bold text-base font-mono">[[SITE]]</a>
|
||||||
|
<div class="group flex gap-2 font-mono text-ctp-subtext1">
|
||||||
|
<span class="hidden group-hover:block">[[TIMEZONE]]</span>
|
||||||
|
<span id="localtime" class="block group-hover:hidden">--:-- --</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main class="my-6">
|
||||||
|
<h1 class="text-4xl font-bold">[[POST_TITLE]]</h1>
|
||||||
|
<span class="text-ctp-subtext0 italic block mb-10">[[DATE]] - <a href="[[REVISIONS]]">view revisions</a></span>
|
||||||
|
[[CONTENT]]
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
<p class="text-center italic text-shadow-ctp-subtext1">© [[YEAR]] [[NAME]]. All rights reserved.</p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue