Initial Release

This commit is contained in:
2026-05-24 19:52:45 -07:00
commit 8242fdbbb3
181 changed files with 10119 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
.env
data
+15
View File
@@ -0,0 +1,15 @@
# Build
FROM golang:1.25.2-alpine3.22 AS build
WORKDIR /app
COPY . .
RUN go build -o homepage.elf main.go
# Runtime
FROM alpine:latest AS runtime
WORKDIR /app
COPY --from=build /app/homepage.elf .
ENV HTTP_ADDRESS="0.0.0.0:8080"
EXPOSE 8080
CMD ["./homepage.elf"]
+68
View File
@@ -0,0 +1,68 @@
package env
import (
"crypto/tls"
"crypto/x509"
"fmt"
"log"
"os"
"time"
)
const (
FILE_MODE = os.FileMode(0770)
)
var (
HTTP_TLS *tls.Config // http: TLS Configuration
HTTP_ADDRESS = envString("HTTP_ADDRESS", "localhost:8080") // http: Address to Listen for Requests on
TLS_ENABLED = envString("TLS_ENABLED", "false") == "true" // http: Enable TLS?
TLS_CERT = envString("TLS_CERT", "tls_crt.pem") // http: Path to TLS Certificate
TLS_KEY = envString("TLS_KEY", "tls_key.pem") // http: Path to TLS Key
TLS_CA = envString("TLS_CA", "tls_ca.pem") // http: Path to TLS CA Bundle
VERSION = fmt.Sprintf("%X", time.Now().Unix())
)
func init() {
log.Println("[env] Using Version Hash:", VERSION)
// Load and Parse TLS Configuration from Disk
if TLS_ENABLED {
cert, err := tls.LoadX509KeyPair(TLS_CERT, TLS_KEY)
if err != nil {
log.Fatalln("[env/tls] Cannot Load Keypair", err)
}
caBytes, err := os.ReadFile(TLS_CA)
if err != nil {
log.Fatalln("[env/tls] Cannot Read CA File", err)
}
caPool := x509.NewCertPool()
if !caPool.AppendCertsFromPEM(caBytes) {
log.Fatalln("[env/tls] Cannot Append Certificates")
}
HTTP_TLS = &tls.Config{
Certificates: []tls.Certificate{cert},
ClientCAs: caPool,
MinVersion: tls.VersionTLS13,
MaxVersion: tls.VersionTLS13,
CipherSuites: []uint16{
tls.TLS_AES_128_GCM_SHA256,
tls.TLS_AES_256_GCM_SHA384,
tls.TLS_CHACHA20_POLY1305_SHA256,
},
}
}
}
// Reads String from Environment
func envString(key, defaultValue string) string {
systemValue := os.Getenv(key)
if systemValue == "" {
if defaultValue == "\x00" {
fmt.Printf("[env] Environment Variable '%s' is undefined\n", key)
os.Exit(2)
}
return defaultValue
}
return systemValue
}
+48
View File
@@ -0,0 +1,48 @@
package env
import (
_ "embed"
"encoding/json"
"log"
"time"
)
type databaseRoot struct {
Site databaseSite `json:"site"`
Articles []databaseArticle `json:"articles"`
}
type databaseSite struct {
Host string `json:"host"`
Name string `json:"name"`
Description string `json:"description"`
Category string `json:"category"`
Locale string `json:"locale"`
Owner string `json:"owner"`
Color string `json:"color"`
}
type databaseArticle struct {
BaseTemplate string `json:"base_template"`
BaseResources string `json:"base_resources"`
BaseBanner string `json:"base_banner"`
Color string `json:"color"`
Slug string `json:"slug"`
Date time.Time `json:"date"`
Title string `json:"title"`
Description string `json:"description"`
Author string `json:"author"`
Tags []string `json:"tags"`
}
var (
//go:embed database.json
embedDatabase []byte
Database databaseRoot
)
func init() {
if err := json.Unmarshal(embedDatabase, &Database); err != nil {
log.Fatalln("[env/db] Parse Database Error:", err)
}
}
+28
View File
@@ -0,0 +1,28 @@
{
"site": {
"host": "https://panca.kz",
"name": "pancakz",
"description": "Tech, Chunibyo \u0026 Other Delusions",
"category": "Personal Blogs",
"locale": "en-US",
"owner": "bakonpancakz",
"color": "77, 88, 255"
},
"articles": [
{
"base_template": "001-personal-email-server.html",
"base_resources": "setting-up-an-email-api",
"base_banner": "banner.gif",
"color": "94, 167, 1",
"slug": "setting-up-an-email-api",
"date": "2024-11-06T19:26:40Z",
"title": "Setting Up an Email API",
"description": "Freaking emails, how do they work?",
"author": "bakonpancakz",
"tags": [
"PERSONAL",
"GO"
]
}
]
}
+5
View File
@@ -0,0 +1,5 @@
module bakonpancakz/homepage
go 1.25.2
require github.com/joho/godotenv v1.5.1
+2
View File
@@ -0,0 +1,2 @@
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+59
View File
@@ -0,0 +1,59 @@
package include
import (
"embed"
"io"
"io/fs"
"log"
"os"
"path"
"strings"
)
var (
//go:embed public/*
embedPublic embed.FS
//go:embed templates/*
embedTemplates embed.FS
Embedded = true
Templates fs.FS
Public fs.FS
)
func init() {
if strings.Contains(os.Args[0], "go-build") {
Embedded = false
}
if Embedded {
Templates = embedTemplates
Public = embedPublic
} else {
log.Println("[include] In Debug Mode, using local filesystem!")
Templates = os.DirFS("include/templates")
Public = os.DirFS("include/public")
}
}
func PreparePath(fsys fs.FS, filepath string) string {
filepath = path.Clean(filepath)
if Embedded {
// If using embed, prepend the folder name automatically if missing
switch {
case fsys == Public && !strings.HasPrefix(filepath, "public/"):
filepath = path.Join("public", filepath)
case fsys == Templates && !strings.HasPrefix(filepath, "templates/"):
filepath = path.Join("templates", filepath)
}
}
return filepath
}
func ReadFile(fsys fs.FS, filepath string) ([]byte, error) {
f, err := fsys.Open(PreparePath(fsys, filepath))
if err != nil {
return nil, err
}
defer f.Close()
return io.ReadAll(f)
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 99.11 94.5" fill="#f0f0f0">
<path d="M99.01,35.85c-.24-.72-.86-1.25-1.62-1.36l-31.83-4.57L51.29,1.11c-.34-.68-1.03-1.11-1.79-1.11h0c-.76,0-1.46.43-1.79,1.12l-14.18,28.85L1.71,34.65c-.75.11-1.38.64-1.61,1.36-.23.72-.04,1.52.51,2.05l23.06,22.41-5.38,31.7c-.13.75.18,1.51.8,1.96.35.25.76.38,1.17.38.32,0,.64-.08.93-.23l28.43-15.01,28.48,14.92c.67.35,1.49.29,2.11-.16.62-.45.92-1.21.79-1.96l-5.49-31.68,22.99-22.48c.54-.53.74-1.33.5-2.05Z" />
</svg>

After

Width:  |  Height:  |  Size: 502 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,5 @@
# \_/
# ()o_o) <( beep boop )
User-agent: *
Disallow:
@@ -0,0 +1,115 @@
// @ts-check
(() => { // Table of Content Highlights
/** @type {HTMLDivElement | null} */
const parentChapter = document.querySelector("div.article-chapters")
/** @type {HTMLDivElement | null} */
const parentArticle = document.querySelector("div.article-content")
if (!parentChapter || !parentArticle) return
/** @type {Array<{ o: number; e: Array<Element | null> }>} */
let scrollSections = []
let scrollHeader = null
let scrollOffset = 0
// Generate Table of Contents
for (const child of parentArticle.children) {
const offset = (child.getBoundingClientRect().top + window.scrollY + scrollOffset) | 0
const normal = child.textContent.toString().toLocaleLowerCase().replaceAll(" ", "-")
if (child.classList.contains("element-header")) {
const anchor = document.createElement("a")
anchor.classList.add("chapter")
anchor.textContent = child.textContent
anchor.href = "#" + normal
child.id = normal
parentChapter.appendChild(anchor)
scrollSections.push({ o: offset, e: [anchor] })
scrollHeader = anchor
scrollOffset += 36 // bandaid fix for desync
}
if (child.classList.contains("element-subheader")) {
const anchor = document.createElement("a")
anchor.classList.add("section")
anchor.textContent = child.textContent
anchor.href = "#" + normal
child.id = normal
parentChapter.appendChild(anchor)
scrollSections.push({ o: offset, e: [scrollHeader, anchor] })
}
}
// Update Highlight when Page Scrolls
function pageScroll() {
const winoffset = window.innerHeight / 5
const position = window.scrollY + winoffset
let closest = null
let closestDist = Infinity
// Find Closest Header
for (const section of scrollSections) {
const dist = Math.abs(position - section.o)
if (dist < closestDist) {
closestDist = dist
closest = section
}
for (const elem of section.e) {
// Reset Styling
elem && elem.removeAttribute("selected")
}
}
// Apply Header Styling
if (closest) {
for (const elem of closest.e) {
elem && elem.setAttribute("selected", "")
}
}
}
window.addEventListener("scroll", pageScroll)
pageScroll()
})();
(() => { // Share Button
/** @type {HTMLButtonElement | null} */
const shareButton = document.querySelector("a#button-share")
/** @type {HTMLDialogElement | null} */
const shareModal = document.querySelector("dialog.layout-dialog")
// UX: Close Modal if Background is clicked or pressed
shareModal && shareModal.addEventListener("click", ev => {
if (ev.target === shareModal) {
shareModal.close()
}
})
// UX: Open Native Modal on Mobile, Custom Modal on Desktop
shareButton && shareButton.addEventListener("click", ev => {
ev.preventDefault()
// Use Native Modal
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent)
if (isMobile) {
const metaTitle = document.querySelector(`meta[property="og:title"]`)?.getAttribute("content")
const metaURL = document.querySelector(`meta[property="og:url"]`)?.getAttribute("content")
if (typeof (metaURL) !== "string" || typeof (metaTitle) !== "string") {
alert("Missing Required Metadata")
return
}
navigator.share({ title: metaTitle, url: metaURL })
return
}
// Use Desktop Modal
if (shareModal) {
shareModal.showModal()
}
})
})();
@@ -0,0 +1,240 @@
div.layout-content {
margin-bottom: 50vw;
}
/* Modal */
dialog.layout-dialog {
position: fixed;
z-index: 999;
width: 100%;
height: 100%;
background-color: rgba(var(--rgb-background), .5);
}
div.layout-modal {
width: 480px;
height: fit-content;
position: relative;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
padding: 16px;
box-sizing: border-box;
}
div.modal-header {
display: flex;
justify-content: space-between;
align-items: start;
margin-bottom: 16px;
}
div.modal-header>p {
color: rgba(var(--rgb-accent), 1);
font-size: x-large;
font-weight: 600;
}
div.modal-header>form>button {
background: transparent;
color: rgba(var(--rgb-text), 1);
font-size: x-large;
border: none;
}
div.modal-header>form>button:hover,
div.modal-header>form>button:focus-visible {
color: rgba(var(--rgb-accent), 1);
cursor: pointer;
}
div.modal-content {
display: flex;
gap: 8px;
justify-content: center;
}
div.modal-content>input {
height: 40px;
padding: 0 8px;
box-sizing: border-box;
border: 1px solid rgba(var(--rgb-accent), 1);
background: transparent;
flex-basis: 100%;
font-family: monospace;
color: rgba(var(--rgb-text), 1);
}
div.modal-content>a {
fill: rgba(var(--rgb-text), 1);
border: 1px solid rgba(var(--rgb-accent), 1);
box-sizing: border-box;
padding: 8px 0;
flex-basis: 100%;
max-width: 40px;
height: 40px;
line-height: 24px;
}
div.modal-content>a:hover,
div.modal-content>a:focus-visible {
background: rgba(var(--rgb-accent), 1);
}
div.modal-content>a>svg {
fill: rgba(var(--rgb-accent), 1);
display: block;
margin: auto;
height: 24px;
}
div.modal-content>a:hover>svg,
div.modal-content>a:focus-visible>svg {
fill: rgba(var(--rgb-text), 1);
}
/* Navigation */
div.article-actions {
display: flex;
justify-content: center;
gap: 8px;
margin-bottom: 8px;
}
div.article-actions>a {
border: 2px solid rgba(var(--rgb-accent), 1);
box-sizing: border-box;
flex-basis: 100%;
padding: 8px 0;
}
div.article-actions>a>svg {
fill: rgba(var(--rgb-accent), 1);
display: block;
margin: auto;
height: 16px;
}
div.article-actions>a:hover,
div.article-actions>a:focus-visible {
background: rgba(var(--rgb-accent), 1);
}
div.article-actions>a:hover>svg,
div.article-actions>a:focus-visible>svg {
fill: rgba(var(--rgb-text), 1);
}
div.article-chapters {
display: grid;
}
div.article-chapters>a.chapter,
div.article-chapters>a.section {
transition: var(--transition-time) ease-in-out all;
text-decoration: none;
}
div.article-chapters>a.chapter {
margin-left: 16px;
transform: skew(0deg);
}
div.article-chapters>a.chapter:hover,
div.article-chapters>a.chapter:focus-visible {
text-decoration: underline;
}
div.article-chapters>a.chapter[selected] {
position: relative;
letter-spacing: 1px;
}
div.article-chapters>a.chapter[selected]::before {
content: '>';
color: rgba(var(--rgb-accent), 1);
font-size: 24px;
font-weight: bold;
position: absolute;
left: -16px;
}
div.article-chapters>a.section {
margin-left: 32px;
font-size: small;
}
div.article-chapters>a.section:hover,
div.article-chapters>a.section:focus-visible {
text-decoration: underline;
}
div.article-chapters>a.section[selected] {
color: rgba(var(--rgb-accent), 1);
letter-spacing: 1px;
}
/* Article */
div.article-header {
width: 100%;
height: fit-content;
display: grid;
justify-items: center;
}
img.header-banner {
width: 100%;
object-fit: cover;
aspect-ratio: 3/1;
border-bottom: 4px solid rgba(var(--rgb-accent), 1);
}
p.header-title {
word-wrap: break-word;
overflow: hidden;
margin: 16px 0 24px 0;
}
div.header-chevrons {
display: flex;
justify-content: space-between;
width: 100%;
}
div.article-content {
padding: 16px;
display: grid;
gap: 8px;
}
/* Elements */
div.element-divider {
position: relative;
background-color: rgba(var(--rgb-accent), 1);
height: 2px;
margin: 16px 0;
}
div.element-divider::before {
content: '';
transform: translate(-50%, -50%);
position: absolute;
height: 16px;
width: 24px;
left: 50%;
top: 50%;
background: url('/public/divider.svg') rgba(var(--rgb-background), 1) no-repeat center;
padding: 0 4px;
}
p.element-header {
font-size: x-large;
font-weight: 500;
color: rgb(var(--rgb-accent));
}
p.element-subheader {
text-decoration: underline;
font-weight: 500;
color: rgb(var(--rgb-accent));
}
@@ -0,0 +1,82 @@
/* Section: Introduction */
div.browser-introduction {
display: grid;
padding: 16px;
box-sizing: border-box;
gap: 8px;
}
p.intro-header {
font-size: large;
text-align: center;
}
img.intro-banner {
margin: auto;
image-rendering: pixelated;
}
p.intro-disclaimer {
font-size: small;
text-align: center;
color: rgba(var(--rgb-text), 0.8);
}
div.intro-dots {
display: flex;
justify-content: center;
font-size: small;
}
/* Section: Items */
/* div.browser-content {} */
a.browser-item {
display: grid;
text-decoration: none;
}
a.browser-item:not(:hover) .effect-rhombus-animated {
animation: none;
}
div.item-background,
div.item-foreground,
a.item-redirect {
grid-row: 1;
grid-column: 1;
}
div.item-background {
width: 100%;
height: 48px;
background-image: var(--background);
background-size: cover;
background-position: center;
filter: grayscale(1) brightness(0.5);
}
div.item-foreground {
display: flex;
align-items: center;
justify-content: space-between;
padding-left: 8px;
}
p.item-description {
font-size: small;
font-style: italic;
}
/* Section: Footer */
div.browser-footer {
padding: 16px;
box-sizing: border-box;
}
div.browser-footer>p {
width: 100%;
text-align: center;
font-family: monospace;
color: rgba(var(--rgb-text), 0.8);
}
@@ -0,0 +1,270 @@
:root {
--transition-time: 200ms;
--rgb-background: 0, 0, 0;
--rgb-text: 240, 240, 240;
--rgb-accent: 70, 70, 70;
}
body {
background-color: rgba(var(--rgb-background), 1);
margin: 0;
padding: 0;
}
::-webkit-scrollbar {
display: none;
}
a,
p,
span {
display: inline-block;
font-family: "Poppins", sans-serif;
line-height: 1.5em;
font-weight: 400;
color: rgba(var(--rgb-text), 1);
padding: 0;
margin: 0;
}
/* Global Effects */
.effect-caution {
position: relative;
border: 4px solid rgba(var(--rgb-accent), 1);
background-color: rgba(var(--rgb-background), 1);
margin-bottom: 12px;
margin-right: 12px;
}
.effect-caution::before {
content: '';
position: absolute;
left: 0;
top: 0;
right: -16px;
bottom: -16px;
box-sizing: border-box;
filter: brightness(0.5);
background: repeating-linear-gradient(45deg, rgba(var(--rgb-accent), 1) 0, rgba(var(--rgb-accent), 1) 8px, rgba(var(--rgb-background), 1) 8px, rgba(var(--rgb-background), 1) 16px);
clip-path: polygon(calc(100% - 11px) 0, 100% 0, 100% 100%, 0 100%, 0 calc(100% - 11px), calc(100% - 11px) calc(100% - 11px));
}
.effect-docked {
font-size: 2em;
font-weight: 700;
text-align: center;
position: relative;
padding: 0 16px;
z-index: 1;
}
.effect-docked::before {
position: absolute;
content: '';
height: 100%;
width: 100%;
top: 0;
left: 0;
box-sizing: border-box;
background: rgba(var(--rgb-accent), 0.7);
clip-path: polygon(12px 60%, calc(100% - 12px) 60%, 100% 100%, 0 100%);
z-index: -1;
}
.effect-rhombus-animated,
.effect-rhombus-left,
.effect-rhombus-right {
position: relative;
font-weight: 500;
padding: 0 16px;
z-index: 1;
}
.effect-rhombus-animated::before,
.effect-rhombus-left::before,
.effect-rhombus-right::before {
position: absolute;
content: '';
height: 100%;
width: 100%;
top: 0;
left: 0;
box-sizing: border-box;
background: rgba(var(--rgb-accent), .8);
z-index: -1;
}
.effect-rhombus-left {
clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 100%, 8px 100%);
}
.effect-rhombus-right {
clip-path: polygon(8px 0, 100% 0, calc(100% - 8px) 100%, 0 100%);
}
.effect-rhombus-animated {
animation: kf-rhombus 1s ease-in-out infinite alternate;
}
@keyframes kf-rhombus {
0% {
clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 100%, 8px 100%);
}
100% {
clip-path: polygon(8px 0, 100% 0, calc(100% - 8px) 100%, 0 100%);
}
}
div.effect-chevron-container {
align-items: end;
}
.effect-chevron-point-left,
.effect-chevron-point-right {
display: inline-block;
background-color: rgba(var(--rgb-accent), 1);
text-decoration: none;
font-weight: 500;
height: 24px;
width: fit-content;
padding: 0 16px;
}
.effect-chevron-point-right {
margin-right: -4px;
clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 50%, calc(100% - 8px) 100%, 0 100%, 8px 50%);
}
div.effect-chevron-container>.effect-chevron-point-right:first-child {
padding-left: 8px;
clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 50%, calc(100% - 8px) 100%, 0 100%);
}
.effect-chevron-point-left {
margin-left: -4px;
clip-path: polygon(8px 0, 100% 0, calc(100% - 8px) 50%, 100% 100%, 8px 100%, 0 50%);
}
div.effect-chevron-container>.effect-chevron-point-left:last-child {
padding-right: 8px;
clip-path: polygon(8px 0, 100% 0, 100% 100%, 8px 100%, 0 50%);
}
span.effect-dot {
height: 16px;
width: 16px;
position: relative;
align-self: center;
border-radius: 100%;
background-color: rgba(var(--color), 0.8);
margin: 0 8px;
}
span.effect-dot::before {
content: '';
height: 10px;
width: 10px;
border-radius: 100%;
position: absolute;
background-color: rgba(var(--color), 1);
transform: translate(-50%, -50%);
left: 50%;
top: 50%;
}
/* Page Layout */
div.layout-wrapper {
display: flex;
gap: 16px;
min-width: 1024px;
max-width: 1024px;
width: 1024px;
margin: 16px auto;
}
div.layout-navigation {
min-width: 286px;
max-width: 286px;
width: 100%;
height: fit-content;
position: sticky;
top: 16px;
}
div.layout-content {
width: 100%;
height: fit-content;
}
/* Layout: Navigation */
div.navigation-header {
padding: 16px;
}
a.navigation-title {
width: 100%;
box-sizing: border-box;
}
p.navigation-description {
width: 100%;
padding: 8px 0;
text-align: center;
box-sizing: border-box;
font-size: small;
}
/* div.navigation-links {} */
a.navigation-item {
width: 100%;
padding: 8px 16px;
box-sizing: border-box;
border-bottom: 4px solid rgba(var(--rgb-accent), 1);
text-decoration: none;
font-size: 20px;
font-weight: 500;
background-size: 100% 300%;
background-repeat: no-repeat;
background-image: var(--background);
}
a.navigation-item:first-child {
border-top: 4px solid rgba(var(--rgb-accent), 1);
}
div.navigation-content {
padding: 16px;
padding-bottom: 0;
}
div.navigation-footer {
display: flex;
padding: 16px;
justify-content: center;
align-items: center;
gap: 4px;
}
a.footer-item {
color: rgba(var(--rgb-accent), 1);
text-decoration: none;
}
a.footer-item:hover,
a.footer-item:focus-visible {
text-decoration: underline;
}
div.footer-views {
width: 100%;
display: flex;
justify-content: center;
gap: 4px;
}
div.footer-views img {
height: 24px;
image-rendering: pixelated;
}
@@ -0,0 +1,122 @@
/* Local Version of following stylesheet to save on bandwidth while developing locally :3 */
/* https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap */
/* devanagari */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/public/fonts/pxiEyp8kv8JHgFVrJJbecmNE.woff2) format('woff2');
unicode-range: U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20B9, U+20F0, U+25CC, U+A830-A839, U+A8E0-A8FF, U+11B00-11B09;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/public/fonts/pxiEyp8kv8JHgFVrJJnecmNE.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/public/fonts/pxiEyp8kv8JHgFVrJJfecg.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* devanagari */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(/public/fonts/pxiByp8kv8JHgFVrLGT9Z11lFc-K.woff2) format('woff2');
unicode-range: U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20B9, U+20F0, U+25CC, U+A830-A839, U+A8E0-A8FF, U+11B00-11B09;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(/public/fonts/pxiByp8kv8JHgFVrLGT9Z1JlFc-K.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(/public/fonts/pxiByp8kv8JHgFVrLGT9Z1xlFQ.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* devanagari */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(/public/fonts/pxiByp8kv8JHgFVrLEj6Z11lFc-K.woff2) format('woff2');
unicode-range: U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20B9, U+20F0, U+25CC, U+A830-A839, U+A8E0-A8FF, U+11B00-11B09;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(/public/fonts/pxiByp8kv8JHgFVrLEj6Z1JlFc-K.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(/public/fonts/pxiByp8kv8JHgFVrLEj6Z1xlFQ.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* devanagari */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(/public/fonts/pxiByp8kv8JHgFVrLCz7Z11lFc-K.woff2) format('woff2');
unicode-range: U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20B9, U+20F0, U+25CC, U+A830-A839, U+A8E0-A8FF, U+11B00-11B09;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(/public/fonts/pxiByp8kv8JHgFVrLCz7Z1JlFc-K.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(/public/fonts/pxiByp8kv8JHgFVrLCz7Z1xlFQ.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@@ -0,0 +1,84 @@
{{ define "ArticleContent" }}
<p class="element-header">Introduction</p>
<p class="element-paragraph">
If you're reading this... I haven't written the article yet :P
</p>
<p class="element-paragraph">
Luctus montes donec mauris. Vehicula suspendisse condimentum massa fringilla at conubia.
Senectus laoreet praesent in, porttitor sollicitudin luctus orci scelerisque egestas malesuada faucibus a.
Arcu imperdiet libero imperdiet nulla libero natoque eu fusce ultricies vehicula nisi.
Torquent lacus sodales inceptos dignissim sed, mauris feugiat erat.
Phasellus at tincidunt sociosqu potenti risus aliquam non litora.
Commodo mi fermentum, id arcu dictum convallis.
Vehicula convallis, proin fames molestie potenti congue penatibus feugiat porttitor.
Habitant justo lacinia, curabitur nascetur!
Dolor nunc accumsan imperdiet penatibus?
Amet cursus faucibus dui vestibulum nascetur quam.
</p>
<div class="element-divider"></div>
<p class="element-header">Prerequisites</p>
<p class="element-paragraph">
Praesent augue ligula amet nisl. Litora lectus consectetur aliquet.
Sociis iaculis dolor nostra pulvinar felis condimentum netus.
Suscipit enim habitant sed eros etiam, ut odio sed.
Fringilla quam tempus nisl natoque parturient dolor curabitur tellus.
Lacus conubia quis inceptos tristique ultricies cum consequat.
Consequat phasellus proin dolor molestie nec quam cursus mollis vel?
Eros tempor sollicitudin tortor litora nunc.
Sem scelerisque semper id ac auctor fusce quis erat imperdiet convallis donec eget!
Pellentesque tempor nunc ipsum imperdiet quam ipsum habitant feugiat.
</p>
<p class="element-subheader">Hosting</p>
<p class="element-paragraph">
Odio nibh penatibus erat sociosqu eget iaculis. Ante curabitur bibendum ligula?
Netus aliquet cursus interdum pharetra. Magnis elementum nunc luctus eu nam sit habitant etiam vestibulum in.
Lacus nisl ullamcorper molestie! Dignissim, vulputate porta pulvinar duis posuere.
</p>
<p class="element-subheader">Domains</p>
<p class="element-paragraph">
Curae; laoreet cum duis turpis, feugiat per integer.
Sit in blandit himenaeos. Penatibus fringilla imperdiet, luctus dapibus elementum.
Felis senectus pulvinar a natoque risus.
</p>
<p class="element-subheader">Credentials</p>
<p class="element-paragraph">
Mauris habitasse nulla accumsan fermentum fermentum.
Magnis amet amet risus senectus in curae;.
Elementum ac montes sagittis vivamus malesuada elementum senectus ultrices scelerisque fusce.
In nulla platea.
</p>
<div class="element-divider"></div>
<p class="element-header">Setup DNS</p>
<p class="element-paragraph">
Nisi vitae vehicula etiam nam auctor, accumsan purus ridiculus molestie.
Ante malesuada condimentum aliquam etiam auctor arcu porttitor parturient nullam accumsan bibendum?
Vitae arcu at mattis. Volutpat himenaeos class vitae elit, luctus sociosqu semper?
Praesent taciti lobortis tortor laoreet magnis tortor sagittis porta facilisi vivamus dictum scelerisque.
Consectetur elit enim malesuada commodo proin facilisi sagittis!
Malesuada quam duis non elit himenaeos sem.
Luctus suspendisse senectus mollis aliquet aliquam pellentesque conubia condimentum suspendisse aptent litora potenti.
Curabitur sociis lacus turpis, laoreet ligula consequat luctus eu auctor auctor sagittis.
Turpis nibh feugiat lorem donec quis bibendum.
Himenaeos sapien interdum aptent.
</p>
<p class="element-header">Setup Linux</p>
<p class="element-paragraph">
Convallis pharetra vitae tincidunt phasellus bibendum porta, praesent dictum aliquet volutpat.
Lobortis semper eget consectetur elementum elit hendrerit natoque mauris taciti ante ac augue.
Ullamcorper viverra vestibulum vehicula nisi metus vestibulum curabitur vehicula et.
Phasellus ligula vitae posuere penatibus ullamcorper lorem phasellus metus.
Scelerisque ac lobortis gravida primis?
Lacus porttitor ullamcorper quisque viverra molestie.
Lacinia bibendum facilisi tempor cubilia felis eros!
Etiam blandit lacinia leo urna.
</p>
{{ end }}
@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="theme-color" content="{{ .Site.Color }}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="A small corner of the web for odd tech experiments and the occasional questionable idea. Enjoy in moderation!">
<link rel="stylesheet" href="/public/styles/default.css?v={{ .Version }}">
<link rel="stylesheet" href="/public/styles/font_poppins.css?v={{ .Version }}">
{{ block "PageStyles" . }}{{ end }}
<title>{{ .Title }} - {{ .Site.Name }}</title>
<meta property="og:locale" content="{{ .Site.Locale }}">
{{ block "PageHead" . }}{{ end }}
</head>
<body style="--rgb-accent: {{ if .Article }}{{ .Article.Color }}{{ else }}{{ .Site.Color }}{{ end }}">
<div class="layout-wrapper">
<!-- Navigation -->
<div class="layout-navigation effect-caution">
<div class="navigation-header">
<a class="navigation-title effect-docked" href="/">{{ .Site.Name }}</a>
<p class="navigation-description">{{ .Site.Description }}</p>
</div>
<div class="navigation-links" style="--background: url('/public/anchors.gif?v={{ .Version}}');">
<a style="background-position-y: 0%;" class="navigation-item" href="/blog">Blog</a>
<a style="background-position-y: 50%;" class="navigation-item" href="http://git.panca.kz/">Codebase</a>
<a style="background-position-y: 100%;" class="navigation-item" href="http://stickers.panca.kz/">Stickerboard</a>
</div>
<div class="navigation-content">
{{ block "NavContent" . }}{{ end }}
</div>
<div class="navigation-footer">
<a class="footer-item" href="/rss.xml">RSS</a>
</div>
</div>
<!-- Content -->
<div class="layout-content effect-caution">
{{ block "PageContent" . }}{{ end }}
</div>
</div>
{{ block "PageBody" . }}{{ end }}
</body>
</html>
@@ -0,0 +1,82 @@
{{ define "PageStyles" }}
<link rel="stylesheet" href="/public/styles/blog_article.css?v={{ .Version }}">
{{ end }}
{{ define "PageHead" }}
<meta property="og:type" content="article">
<meta property="og:title" content="{{ .Article.Title }}">
<meta property="og:image" content="{{ .Site.Host }}{{ .Article.BaseBanner }}">
<meta property="og:url" content="{{ .Site.Host }}/blog/{{ .Article.Slug }}">
<meta property="og:description" content="{{ .Article.Description }}">
{{ end }}
{{ define "PageBody" }}
<dialog class="layout-dialog">
<div class="layout-modal effect-caution">
<div class="modal-header">
<p>Share</p>
<form method="dialog" autofocus>
<button>&#10005;</button>
</form>
</div>
<div class="modal-content">
<input type="text" value="{{ .Site.Host }}/blog/{{ .Article.Slug }}" onclick="this.select()" readonly>
<a title="Share on X" target="_blank" href="https://twitter.com/intent/tweet?url={{ .Site.Host }}/blog/{{ .Article.Slug }}&text={{ .Article.Title }}&via={{ .Site.Owner }}">
{{ template "logo_x.svg" . }}
</a>
<a title="Share on Facebook" target="_blank" href="https://www.facebook.com/sharer/sharer.php?u={{ .Site.Host }}/blog/{{ .Article.Slug }}">
{{ template "logo_facebook.svg" . }}
</a>
<a title="Share on LinkedIn" target="_blank" href="https://www.linkedin.com/shareArticle?url={{ .Site.Host }}/blog/{{ .Article.Slug }}">
{{ template "logo_linkedin.svg" . }}
</a>
</div>
</div>
</dialog>
<script src="/public/scripts/blog_article.js?v={{ .Version }}"></script>
{{ end }}
{{ define "NavContent" }}
<div class="article-actions">
<a title="Visit Homepage" href="/blog">
{{ template "icon_home.svg" . }}
</a>
<a title="Return To Top" href="#" class="mobile-hide">
{{ template "icon_top.svg" . }}
</a>
<a title="Share Article" href="{{ .Site.Host }}/blog/{{ .Article.Slug }}" id="button-share">
{{ template "icon_share.svg" . }}
</a>
</div>
<div class="article-chapters">
<!-- To Be Generated by Article Script -->
</div>
{{ end }}
{{ define "PageContent" }}
<h1 id="#" hidden><!-- Top Anchor --></h1>
<div class="article-header">
<img class="header-banner" alt="Article Banner" src="/public/articles/{{ .Article.BaseResources }}/{{ .Article.BaseBanner }}?v={{ $.Version }}">
<p class="header-title effect-docked">{{ .Article.Title }}</p>
<div class="header-chevrons">
<div class="effect-chevron-container header-about">
<span class="effect-chevron-point-right">{{ .Article.Author }}</span>
<span class="effect-chevron-point-right">{{ .Article.Date.Format "Jan 02 2006" }}</span>
</div>
<div class="effect-chevron-container header-tags">
{{ range .Article.Tags }}
<span class="effect-chevron-point-left">{{ . }}</span>
{{ end }}
</div>
</div>
</div>
<div class="article-content">
{{ block "ArticleContent" . }}{{ end }}
</div>
{{ end }}
@@ -0,0 +1,71 @@
{{ template "base.html" . }}
{{ define "PageStyles" }}
<link rel="stylesheet" href="/public/styles/blog_browser.css?v={{ .Version }}">
<link rel="stylesheet" href="/public/styles/blog_article.css?v={{ .Version }}">
{{ end }}
{{ define "PageContent" }}
<!-- Introduction & Disclaimer -->
<div class="browser-introduction">
<p class="intro-header effect-rhombus-right">Welcome to my blog!</p>
<img class="intro-banner" src="/public/banner_blog.gif?v={{ .Version }}" alt="Blog Banner" width="520" height="292">
<p class="intro-paragraph">
This corner of the internet contains ramblings about random experiments
that more than likely occured on a bright sunday afternoon. Nothing here
is meant to be perfect, but I do hope to provide the odd spark of insight.
<br><br>
I assume no responsibility for bodily harm, wasted time, or raised eyebrows
that may result from consuming these articles. Enjoy in Moderation! Have fun!
</p>
<div class="element-divider"></div>
<div class="intro-dots">
<span class="effect-dot" style="--color: 94, 167, 1;"></span>
<p>Personal</p>
<span class="effect-dot" style="--color: 0, 167, 170;"></span>
<p>Update</p>
<span class="effect-dot" style="--color: 255, 0, 91;"></span>
<p>Guide</p>
</div>
</div>
<!-- Article List -->
<div class="browser-content">
{{ range $.Articles }}
<a class="browser-item" href="/blog/{{ .Slug }}" style="--rgb-accent: {{ .Color }};">
<div class="item-background" style="--background: url('/public/articles/{{ .BaseResources }}/{{ .BaseBanner }}?v={{ $.Version }}');"></div>
<div class="item-foreground">
<p class="effect-rhombus-left effect-rhombus-animated">{{ .Title }}</p>
<div class="item-tags effect-chevron-container">
{{ range .Tags }}
<span class="effect-chevron-point-left">{{ . }}</span>
{{ end }}
<span class="effect-chevron-point-left">{{ .Date.Format "Jan 02 2006" }}</span>
</div>
</div>
</a>
{{ end }}
</div>
<!-- Fortune Cookie -->
<div class="browser-footer">
<p id="browser-message">If you're reading this, you have JavaScript disabled. Paranoid much? :P</p>
<script>
const monitor = document.querySelector("p#browser-message")
const messages = [
"Did you eat something sweet today? Everybody deserves a sweet treat, especially you. :)",
"Make sure you clean your keyboard, it's amazingly disgusting what you can find in there.",
"Religion aside even God rested on the 7th day, are you treating yourself well?",
"If bored try learning about how your favorite Game Console OS works, or is that weird?",
"Your projects miss you... maybe poke around? You might rediscover something!",
"If somebody has done it before, you can too! And if they got close, you can get closer!",
"Bad Idea Counter -> {{ len .Articles }}",
]
monitor.textContent = messages[Math.floor(Math.random() * messages.length)]
</script>
</div>
{{ end }}
@@ -0,0 +1,76 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex">
<title>Page Not Found</title>
<style>
:root{
--color: {{ .Site.Color }};
}
body {
background-color: black;
margin: 0;
}
a,
p {
font-family: "MS Gothic", monospace;
line-height: 16px;
}
p {
color: #f0f0f0;
margin: 4px 0;
}
a,
a:visited {
color: rgba(var(--color), 1);
display: inline-block;
text-align: center;
width: 50%;
text-decoration: none;
}
a:hover,
a:focus-visible {
text-decoration: underline;
outline: none;
}
div {
width: 200px;
text-align: center;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
</style>
</head>
<body>
<div>
<p>404: Page Not Found </p>
<br>
<p>.-.</p>
<br>
<a href="javascript:window.history.back()">Go Back</a><a href="/">Homepage</a>
</div>
<script>
// Note: In MS Gothic, backslash shows as ¥ on Windows
const catalog = [
".(^Д^)/", "(・.・)", "(°Δ°)b", "(・_・)", "(^_^)", "(>_<)",
"(o^^)o", "(;-;)", ".(≧o≦)/", "(o_o)", "(^-^*)", "(='X'=)",
"( ̄へ ̄)", "(★ω★)", "ᓚᘏᗢ", "✿◠‿◠", "(●'◡'●)", "◑﹏◐"
];
const kaomoji = catalog[Math.floor(Math.random() * catalog.length)];
document.querySelectorAll("p")[1].textContent = kaomoji;
</script>
</body>
</html>
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<atom:link href="{{ .Site.Host}}/rss.xml" rel="self" type="application/rss+xml"></atom:link>
<link>{{ .Site.Host }}</link>
<title><![CDATA[{{ .Site.Name }}]]></title>
<description><![CDATA[{{ .Site.Description }}]]></description>
<category>{{ .Site.Category }}</category>
<language>{{ .Site.Locale }} </language>
<generator>teto</generator>
<copyright>Copyright 2025 {{ .Site.Owner }}</copyright>
<lastBuildDate>{{ .BuildDate.UTC.Format .RFC1123 }}</lastBuildDate>
</channel>
{{ range $index, $elem := .Articles }}
<item>
<link>{{ $.Site.Host }}/{{ $elem.Slug }}</link>
<guid isPermaLink="true">{{ $.Site.Host }}/{{ $elem.Slug }}</guid>
<title><![CDATA[{{ $elem.Title }}]]></title>
<description><![CDATA[{{ $elem.Description }}]]></description>
<pubDate>{{ $elem.Date.UTC.Format $.RFC1123 }}</pubDate>
{{ range $i, $tag := $elem.Tags }}
<category>{{ $tag }}</category>
{{ end }}
</item>
{{ end }}
</rss>
@@ -0,0 +1,21 @@
{{ template "base.html" . }}
{{ define "PageContent" }}
<style>
div.placeholder {
width: 100%;
height: 100%;
min-height: 480px;
display: grid;
justify-content: center;
justify-items: center;
align-content: center;
gap: 32px;
}
</style>
<div class="placeholder" style="text-align: center;">
<img style="image-rendering: pixelated;" src="/public/banner_home.gif?v={{ .Version }}" alt="Lost Sprite">
<p>I intended to put something here, but I couldn't come up with anything....</p>
<p>So check out my <a href="/blog">blog</a>, <a href="https://git.panca.kz/">code</a> or <a href="https://stickers.panca.kz/">stickerboard</a>!</p>
</div>
{{ end }}
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 32">
<!-- @bakonpancakz -->
<polygon points="20 0 0 14 6 14 6 32 16 32 16 21 24 21 24 32 34 32 34 14 40 14 20 0" />
</svg>

After

Width:  |  Height:  |  Size: 186 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30.99 30.99">
<!-- @bakonpancakz -->
<polygon points="26.22 14.92 26.22 27.25 3.73 27.25 3.73 4.75 16.05 4.75 12.31 1.01 0 1.01 0 1.03 0 3.82 0 28.18 0 30.97 0 30.99 29.95 30.99 29.95 30.97 29.98 30.97 29.98 18.68 26.22 14.92" />
<polygon points="14.99 0 21.57 6.59 13.57 14.59 16.4 17.41 24.4 9.41 30.99 16 30.99 0 14.99 0" />
</svg>

After

Width:  |  Height:  |  Size: 399 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 17.51">
<!-- @bakonpancakz -->
<polygon points="15 17.51 3 17.51 0 8.01 18 8.01 15 17.51" />
<rect y="4" width="18" height="3" />
<rect width="18" height="3" />
</svg>

After

Width:  |  Height:  |  Size: 239 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<!-- @bakonpancakz -->
<polygon points="4.69 20.68 13 20.69 13 32 18.99 31.99 18.99 20.68 27.31 20.68 16 6.74 4.69 20.68" />
<rect width="32" height="3.74" />
</svg>

After

Width:  |  Height:  |  Size: 238 B

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
<mask id="icon">
<path fill="white" d="M500,250C500,111.93,388.07,0,250,0S0,111.93,0,250c0,117.24,80.72,215.62,189.61,242.64v-166.24h-51.55v-76.4h51.55v-32.92c0-85.09,38.51-124.53,122.05-124.53,15.84,0,43.17,3.11,54.35,6.21v69.25c-5.9-.62-16.15-.93-28.88-.93-40.99,0-56.83,15.53-56.83,55.9v27.02h81.66l-14.03,76.4h-67.63v171.77c123.79-14.95,219.71-120.35,219.71-248.17Z" />
<path fill="black" d="M347.92,326.4l14.03-76.4h-81.66v-27.02c0-40.37,15.84-55.9,56.83-55.9,12.73,0,22.98.31,28.88.93v-69.25c-11.18-3.11-38.51-6.21-54.35-6.21-83.54,0-122.05,39.44-122.05,124.53v32.92h-51.55v76.4h51.55v166.24c19.34,4.8,39.57,7.36,60.39,7.36,10.25,0,20.36-.63,30.29-1.83v-171.77h67.63Z" />
</mask>
<rect mask="url(#icon)" width="500" height="500" />
</svg>

After

Width:  |  Height:  |  Size: 835 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 382 382">
<path d="M347.445,0H34.555C15.471,0,0,15.471,0,34.555v312.889C0,366.529,15.471,382,34.555,382h312.889 C366.529,382,382,366.529,382,347.444V34.555C382,15.471,366.529,0,347.445,0z M118.207,329.844c0,5.554-4.502,10.056-10.056,10.056 H65.345c-5.554,0-10.056-4.502-10.056-10.056V150.403c0-5.554,4.502-10.056,10.056-10.056h42.806 c5.554,0,10.056,4.502,10.056,10.056V329.844z M86.748,123.432c-22.459,0-40.666-18.207-40.666-40.666S64.289,42.1,86.748,42.1 s40.666,18.207,40.666,40.666S109.208,123.432,86.748,123.432z M341.91,330.654c0,5.106-4.14,9.246-9.246,9.246H286.73 c-5.106,0-9.246-4.14-9.246-9.246v-84.168c0-12.556,3.683-55.021-32.813-55.021c-28.309,0-34.051,29.066-35.204,42.11v97.079 c0,5.106-4.139,9.246-9.246,9.246h-44.426c-5.106,0-9.246-4.14-9.246-9.246V149.593c0-5.106,4.14-9.246,9.246-9.246h44.426 c5.106,0,9.246,4.14,9.246,9.246v15.655c10.497-15.753,26.097-27.912,59.312-27.912c73.552,0,73.131,68.716,73.131,106.472 L341.91,330.654L341.91,330.654z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300.251">
<path d="M178.57 127.15 290.27 0h-26.46l-97.03 110.38L89.34 0H0l117.13 166.93L0 300.25h26.46l102.4-116.59 81.8 116.59h89.34M36.01 19.54H76.66l187.13 262.13h-40.66"/>
</svg>

After

Width:  |  Height:  |  Size: 243 B

+122
View File
@@ -0,0 +1,122 @@
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"path"
"sync"
"syscall"
"time"
"bakonpancakz/homepage/env"
"bakonpancakz/homepage/include"
"bakonpancakz/homepage/routes"
_ "github.com/joho/godotenv/autoload"
)
func main() {
// Startup Services
var stopCtx, stop = context.WithCancel(context.Background())
var stopWg sync.WaitGroup
go SetupHTTP(stopCtx, &stopWg)
// Await Shutdown Signal
cancel := make(chan os.Signal, 1)
signal.Notify(cancel, syscall.SIGINT, syscall.SIGTERM)
<-cancel
stop()
// Begin Shutdown Process
timeout, finish := context.WithTimeout(context.Background(), time.Minute)
defer finish()
go func() {
<-timeout.Done()
if timeout.Err() == context.DeadlineExceeded {
log.Fatalln("[main] Cleanup timeout! Exiting now.")
}
}()
stopWg.Wait()
log.Println("[main] All done, bye bye!")
os.Exit(0)
}
func SetupHTTP(stop context.Context, await *sync.WaitGroup) {
notFoundHandler := routes.ServeStaticTemplate(&routes.ServeTemplateInfo{
FileSystem: include.Templates,
FilePaths: []string{"generate_404.html"},
Filename: "generate_404.html",
Literals: map[string]any{
"Site": env.Database.Site,
},
})
mux := http.NewServeMux()
mux.HandleFunc("/", routes.GET_Index(notFoundHandler))
mux.HandleFunc("/blog", routes.ServeStaticTemplate(&routes.ServeTemplateInfo{
FileSystem: include.Templates,
FilePaths: []string{"base.html", "blog_browser.html"},
Filename: "blog_browser.html",
Literals: map[string]any{
"Title": "Browser",
"Version": env.VERSION,
"Site": env.Database.Site,
"Articles": env.Database.Articles,
},
}))
mux.HandleFunc("/rss.xml", routes.ServeStaticTemplate(&routes.ServeTemplateInfo{
FileSystem: include.Templates,
FilePaths: []string{"generate_rss.html"},
Filename: "generate_rss.html",
ContentType: "application/rss+xml",
Literals: map[string]any{
"Site": env.Database.Site,
"Articles": env.Database.Articles,
"RFC1123": time.RFC1123,
"BuildDate": time.Now(),
},
}))
mux.HandleFunc("/blog/{slug}", routes.PrepareBlogArticles(notFoundHandler))
mux.HandleFunc("/favicon.ico", routes.ServeStaticFile("favicon.ico"))
mux.HandleFunc("/robots.txt", routes.ServeStaticFile("robots.txt"))
mux.HandleFunc("/public/{path...}", func(w http.ResponseWriter, r *http.Request) {
routes.ServePublicFile(w, r, path.Clean(r.PathValue("path")))
})
svr := http.Server{
Handler: mux,
Addr: env.HTTP_ADDRESS,
TLSConfig: env.HTTP_TLS,
MaxHeaderBytes: 4096,
IdleTimeout: 5 * time.Second,
ReadHeaderTimeout: 5 * time.Second,
WriteTimeout: 30 * time.Second,
ReadTimeout: 30 * time.Second,
}
// Shutdown Logic
await.Add(1)
go func() {
defer await.Done()
<-stop.Done()
svr.Shutdown(context.Background())
log.Println("[http] Cleaned up HTTP")
}()
// Server Startup
var err error
if env.TLS_ENABLED {
log.Printf("[http] Bound HTTPS - %s\n", svr.Addr)
err = svr.ListenAndServeTLS("", "")
} else {
log.Printf("[http] Bound HTTP - %s\n", svr.Addr)
err = svr.ListenAndServe()
}
if err != http.ErrServerClosed {
log.Fatalln("[http] Listen Error:", err)
}
}
+105
View File
@@ -0,0 +1,105 @@
package routes
import (
"bakonpancakz/homepage/env"
"bakonpancakz/homepage/include"
"bytes"
"compress/gzip"
"fmt"
"log"
"net/http"
"strings"
"sync"
"text/template"
)
var articleCache sync.Map
func PrepareBlogArticles(notFoundHandler HttpHandler) HttpHandler {
return func(w http.ResponseWriter, r *http.Request) {
for _, art := range env.Database.Articles {
if strings.Compare(art.Slug, r.PathValue("slug")) == 0 {
compress := strings.Contains(r.Header.Get("Accept-Encoding"), "gzip")
cacheKeyStandard := fmt.Sprintf("art_%s_%t", art.Slug, true)
cacheKeyCompress := fmt.Sprintf("art_%s_%t", art.Slug, false)
// Check Browser Cache
if r.Header.Get("If-None-Match") == env.VERSION {
w.WriteHeader(http.StatusNotModified)
return
}
// Check Template Cache
w.Header().Set("ETag", env.VERSION)
w.Header().Set("Content-Type", "text/html")
w.Header().Set("Cache-Control", "public, max-age=3600, must-revalidate")
if compress {
if v, ok := articleCache.Load(cacheKeyCompress); ok {
w.Header().Set("Content-Encoding", "gzip")
w.Write(v.([]byte))
return
}
} else {
if v, ok := articleCache.Load(cacheKeyStandard); ok {
w.Write(v.([]byte))
return
}
}
// Render Document
tmpl, err := template.ParseFS(
include.Templates,
include.PreparePath(include.Templates, "base.html"),
include.PreparePath(include.Templates, "blog_article.html"),
include.PreparePath(include.Templates, fmt.Sprint("articles/", art.BaseTemplate)),
include.PreparePath(include.Templates, "icons/logo_facebook.svg"),
include.PreparePath(include.Templates, "icons/logo_linkedin.svg"),
include.PreparePath(include.Templates, "icons/logo_x.svg"),
include.PreparePath(include.Templates, "icons/icon_top.svg"),
include.PreparePath(include.Templates, "icons/icon_home.svg"),
include.PreparePath(include.Templates, "icons/icon_share.svg"),
include.PreparePath(include.Templates, "icons/icon_stack.svg"),
)
if err != nil {
log.Println("[routes/blog] Template Error:", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
var b bytes.Buffer
if err = tmpl.ExecuteTemplate(&b, "base.html", map[string]any{
"Title": art.Title,
"Version": env.VERSION,
"Site": env.Database.Site,
"Article": art,
}); err != nil {
log.Println("[routes/blog] Template Error:", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
// Compress Document
var c bytes.Buffer
g := gzip.NewWriter(&c)
g.Write(b.Bytes())
g.Close()
if include.Embedded {
articleCache.Store(cacheKeyStandard, b.Bytes())
articleCache.Store(cacheKeyCompress, c.Bytes())
}
// Send Document
if compress {
w.Header().Set("Content-Encoding", "gzip")
w.Write(c.Bytes())
} else {
w.Write(b.Bytes())
}
return
}
}
notFoundHandler(w, r)
}
}
+33
View File
@@ -0,0 +1,33 @@
package routes
import (
"bakonpancakz/homepage/env"
"bakonpancakz/homepage/include"
"net/http"
)
func GET_Index(notFoundHandler HttpHandler) HttpHandler {
handler := ServeStaticTemplate(&ServeTemplateInfo{
FileSystem: include.Templates,
FilePaths: []string{"base.html", "homepage.html"},
Filename: "homepage.html",
Literals: map[string]any{
"Title": "Home",
"Version": env.VERSION,
"Site": env.Database.Site,
},
})
return func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
handler(w, r)
return
}
if r.Method == http.MethodGet {
notFoundHandler(w, r)
return
}
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
+197
View File
@@ -0,0 +1,197 @@
package routes
import (
"bakonpancakz/homepage/include"
"bytes"
"compress/gzip"
"crypto/md5"
"fmt"
"io/fs"
"log"
"mime"
"net/http"
"os"
"path"
"strings"
"sync"
"text/template"
)
type HttpHandler func(w http.ResponseWriter, r *http.Request)
var hashTable sync.Map
type ServeTemplateInfo struct {
FileSystem fs.FS
FilePaths []string
Filename string
Literals map[string]any
StatusCode int
ContentType string
}
func prepareStaticTemplate(info *ServeTemplateInfo, ds *[]byte, dc *[]byte, dh *string) error {
// Render Document
tmpl, err := template.ParseFS(info.FileSystem, info.FilePaths...)
if err != nil {
return err
}
var b bytes.Buffer
if err := tmpl.ExecuteTemplate(&b, info.Filename, info.Literals); err != nil {
return err
}
// Compress Document
var c bytes.Buffer
g := gzip.NewWriter(&c)
g.Write(b.Bytes())
g.Close()
// Store Document
*ds = b.Bytes()
*dc = c.Bytes()
*dh = fmt.Sprintf("%X", md5.Sum(b.Bytes()))
return nil
}
func ServeStaticTemplate(info *ServeTemplateInfo) func(w http.ResponseWriter, r *http.Request) {
// Defaults
if info.StatusCode == 0 {
info.StatusCode = http.StatusOK
}
if info.ContentType == "" {
info.ContentType = "text/html"
}
for i, p := range info.FilePaths {
info.FilePaths[i] = include.PreparePath(info.FileSystem, p)
}
// Render Document
var (
documentStandard []byte
documentCompress []byte
documentHash string
)
if err := prepareStaticTemplate(info, &documentStandard, &documentCompress, &documentHash); err != nil {
log.Fatalln("[routes/template] Cannot Prepare Template:", err)
}
// Serve Function
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
if !include.Embedded {
if err := prepareStaticTemplate(info, &documentStandard, &documentCompress, &documentHash); err != nil {
log.Println("[routes/template] Refresh Error:", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}
// Check Browser Cache
if r.Header.Get("If-None-Match") == documentHash {
w.WriteHeader(http.StatusNotModified)
return
}
// Send Document
w.Header().Set("ETag", documentHash)
w.Header().Set("Content-Type", info.ContentType)
w.Header().Set("Cache-Control", "public, max-age=3600, must-revalidate")
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
w.Header().Set("Content-Encoding", "gzip")
w.Write(documentCompress)
} else {
w.Write(documentStandard)
}
}
}
func ServeStaticFile(filepath string) HttpHandler {
var (
fileHash string
fileData []byte
fileMime string
)
// Prepare File
b, err := include.ReadFile(include.Public, filepath)
if err != nil {
log.Fatalln("[routes/static] Cannot Prepare File:", filepath, err)
}
fileMime = mime.TypeByExtension(path.Ext(filepath))
fileHash = fmt.Sprintf("%X", md5.Sum(b))
fileData = b
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
if r.Header.Get("If-None-Match") == fileHash {
w.WriteHeader(http.StatusNotModified)
return
}
w.Header().Add("ETag", fileHash)
w.Header().Add("X-Robots-Tag", "noindex")
w.Header().Add("Content-Type", fileMime)
w.Header().Add("Cache-Control", "public, max-age=604800, immutable")
w.Write(fileData)
}
}
func ServePublicFile(w http.ResponseWriter, r *http.Request, filepath string) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
// Check Cache Hash
if hash, ok := hashTable.Load(filepath); ok {
if r.Header.Get("If-None-Match") == hash.(string) {
w.WriteHeader(http.StatusNotModified)
return
}
}
// Read File Contents
filedata, err := include.ReadFile(include.Public, filepath)
if err != nil {
if os.IsNotExist(err) {
w.WriteHeader(http.StatusNotFound)
return
}
log.Println("[http] Read Asset Error:", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
// Generate Cache Hash
filehash := fmt.Sprintf("%X", md5.Sum(filedata))
hashTable.Store(filepath, filehash)
fileMime := mime.TypeByExtension(path.Ext(filepath))
// Send Contents
w.Header().Add("ETag", filehash)
w.Header().Add("X-Robots-Tag", "noindex")
w.Header().Add("Content-Type", fileMime)
w.Header().Add("Cache-Control", "public, max-age=604800, immutable")
if len(filedata) < (1<<20) &&
!strings.Contains(fileMime, "image") &&
!strings.Contains(fileMime, "audio") &&
!strings.Contains(fileMime, "video") &&
strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
w.Header().Set("Content-Encoding", "gzip")
g := gzip.NewWriter(w)
g.Write(filedata)
g.Close()
} else {
w.Write(filedata)
}
}