Compare commits

...

11 Commits

Author SHA1 Message Date
bebb2f08f6 Styling, elements. 2024-05-28 19:52:04 +02:00
a92da686c5 Hightlight color. 2024-05-28 17:15:26 +02:00
8952108bb5 Started design. 2024-05-28 17:11:43 +02:00
752aeb9082 Files 2024-05-28 16:16:15 +02:00
8e173b9797 Removed config from repo. 2024-05-28 16:15:44 +02:00
03cbe99dad Switched to Shifu. 2024-05-28 16:12:14 +02:00
54658e707d Blog article and small improvements. 2021-09-12 20:04:36 +02:00
1429a5f924 CV. 2021-09-12 19:27:19 +02:00
85d6c82ad3 Legal. 2021-09-12 19:07:48 +02:00
4d1df3b3ea Added font, styled left part of homepage. 2021-09-12 18:10:35 +02:00
86681f0875 Basic Oogway setup. 2021-09-12 17:11:52 +02:00
54 changed files with 306 additions and 1961 deletions

4
.gitignore vendored
View File

@@ -1,3 +1 @@
.idea/ config.json
static/blog/
geodb/

View File

@@ -1,25 +0,0 @@
FROM golang AS build
COPY . /go/src/github.com/Kugelschieber/marvinblum
WORKDIR /go/src/github.com/Kugelschieber/marvinblum
RUN apt-get update && apt-get upgrade -y
ENV GOPATH=/go
ENV CGO_ENABLED=0
RUN go build -ldflags "-s -w" main.go
FROM alpine
RUN apk update && \
apk upgrade && \
apk add --no-cache && \
apk add ca-certificates && \
rm -rf /var/cache/apk/*
COPY --from=build /go/src/github.com/Kugelschieber/marvinblum /app
WORKDIR /app
# default config
ENV MB_LOGLEVEL=info
ENV MB_ALLOWED_ORIGINS=*
ENV MB_HOST=0.0.0.0:8888
EXPOSE 8888
CMD ["/app/main"]

21
LICENSE
View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2020 Marvin Blum
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.

View File

@@ -1,3 +1,7 @@
# marvinblum.de # marvinblum.de
My website. My [website](https://marvinblum.de) built with [Shifu](https://github.com/emvi/shifu).
## Running Locally
Copy `dev_config.json` to `config.json` and start Shifu using `shifu` inside the project directory.

1
assets/js/main.js Normal file
View File

@@ -0,0 +1 @@
console.log("Hello from Shifu!");

35
assets/scss/_font.scss Normal file
View File

@@ -0,0 +1,35 @@
@font-face {
font-family: "Open Sans";
font-style: italic;
font-weight: normal;
font-stretch: 100%;
font-display: swap;
src: url("../font/OpenSans-Italic.ttf") format("truetype");
}
@font-face {
font-family: "Open Sans";
font-style: normal;
font-weight: normal;
font-stretch: 100%;
font-display: swap;
src: url("../font/OpenSans-Regular.ttf") format("truetype");
}
@font-face {
font-family: "Open Sans";
font-style: normal;
font-weight: bold;
font-stretch: 100%;
font-display: swap;
src: url("../font/OpenSans-Bold.ttf") format("truetype");
}
@font-face {
font-family: "Open Sans";
font-style: italic;
font-weight: bold;
font-stretch: 100%;
font-display: swap;
src: url("../font/OpenSans-BoldItalic.ttf") format("truetype");
}

119
assets/scss/main.scss Normal file
View File

@@ -0,0 +1,119 @@
@import "_font.scss";
$borderRadius: 8px;
$textColor: #0f0f0f;
$lightGray: #e6e6e6;
$highlight: #82b3db;
* {
box-sizing: border-box;
font-family: "Open Sans", sans-serif;
font-size: 18px;
line-height: 1.5;
color: $textColor;
}
body {
padding: 80px 40px;
max-width: 1000px;
margin: 0 auto;
}
h1, h2 {
font-size: 32px;
margin: 0 0 40px 0;
font-weight: normal;
a {
font-size: 32px;
}
}
h2 {
font-size: 26px;
}
a {
text-decoration: none;
transition: all 0.3s;
color: $highlight;
&:hover {
color: $textColor;
}
}
nav {
display: flex;
justify-content: space-between;
align-items: center;
margin: 0 0 80px 0;
ul {
display: flex;
gap: 20px;
margin: 0;
padding: 0;
li {
list-style: none;
margin: 0;
padding: 0;
a {
color: $textColor;
&:hover {
color: $highlight;
}
}
}
}
.contact {
display: inline-block;
padding: 8px 16px;
border-radius: $borderRadius;
border: 2px solid $highlight;
color: $textColor;
transition: all 0.3s;
color: $highlight;
&:hover {
background: $highlight;
color: #fff;
}
}
}
section {
margin: 0 0 80px 0;
}
footer {
margin: 0 0 80px 0;
padding: 40px 0;
border-style: solid;
border-width: 2px 0 0 0;
border-color: $lightGray;
}
.intro {
article {
display: flex;
gap: 40px;
justify-content: flex-start;
align-items: center;
h1 {
margin: 0;
}
img {
max-width: 256px;
max-height: 256px;
aspect-ratio: 1;
border-radius: $borderRadius;
}
}
}

View File

@@ -1,206 +0,0 @@
package blog
import (
"fmt"
"github.com/Kugelschieber/marvinblum/tpl"
emvi "github.com/emvi/api-go"
"github.com/emvi/logbuch"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"regexp"
"sync"
"time"
)
const (
blogCacheTime = time.Minute * 15
blogFileCache = "static/blog"
maxLatestArticles = 3
)
var (
linkRegex = regexp.MustCompile(`(?iU)href="/read/([^"]+)"`)
attachmentRegex = regexp.MustCompile(`(?iU)(href|src)="([^"]+)/api/v1/content/([^"]+)"`)
attachmentURLRegex = regexp.MustCompile(`(?iU)(href|src)="([^"]+/api/v1/content/)([^"]+)"`)
)
type Blog struct {
client *emvi.Client
articles []emvi.Article // sorted by date published (descending)
articleMap map[string]emvi.Article // id -> article
articlesYear map[int][]emvi.Article // year -> articleMap
nextUpdate time.Time
cache *tpl.Cache
m sync.Mutex
}
func NewBlog(cache *tpl.Cache) *Blog {
logbuch.Info("Initializing blog")
b := &Blog{cache: cache}
b.client = emvi.NewClient(os.Getenv("MB_EMVI_CLIENT_ID"),
os.Getenv("MB_EMVI_CLIENT_SECRET"),
os.Getenv("MB_EMVI_ORGA"),
nil)
b.nextUpdate = time.Now().Add(blogCacheTime)
if err := os.MkdirAll(blogFileCache, 0755); err != nil {
logbuch.Error("Error creating blog file cache directory", logbuch.Fields{"err": err})
}
b.loadArticles()
return b
}
func (blog *Blog) GetArticle(id string) emvi.Article {
blog.refreshIfRequired()
return blog.articleMap[id]
}
func (blog *Blog) GetArticles() map[int][]emvi.Article {
blog.refreshIfRequired()
return blog.articlesYear
}
func (blog *Blog) GetLatestArticles() []emvi.Article {
blog.refreshIfRequired()
articles := make([]emvi.Article, 0, 3)
i := 1
for _, v := range blog.articles {
articles = append(articles, v)
i++
if i > maxLatestArticles {
break
}
}
return articles
}
func (blog *Blog) loadArticles() {
blog.m.Lock()
defer blog.m.Unlock()
logbuch.Info("Refreshing blog articleMap...")
articles, offset, count := make([]emvi.Article, 0), 0, 1
var err error
for count > 0 {
var results []emvi.Article
results, _, err = blog.client.FindArticles("", &emvi.ArticleFilter{
BaseSearch: emvi.BaseSearch{Offset: offset},
Tags: "blog",
SortPublished: emvi.SortDescending,
})
if err != nil {
logbuch.Error("Error loading blog article", logbuch.Fields{"err": err})
break
}
offset += len(results)
count = len(results)
for _, article := range results {
articles = append(articles, article)
}
}
if err == nil {
for i, article := range articles {
article.LatestArticleContent = blog.loadArticle(article)
articles[i] = article
}
blog.setArticles(articles)
}
blog.nextUpdate = time.Now().Add(blogCacheTime)
logbuch.Info("Done", logbuch.Fields{"count": len(articles)})
}
func (blog *Blog) loadArticle(article emvi.Article) *emvi.ArticleContent {
existingArticle := blog.articleMap[article.Id]
var content *emvi.ArticleContent
if len(existingArticle.Id) == 0 || !existingArticle.ModTime.Equal(article.ModTime) {
var err error
_, content, _, err = blog.client.GetArticle(article.Id, article.LatestArticleContent.LanguageId, 0)
if err != nil {
logbuch.Error("Error loading article", logbuch.Fields{"err": err, "id": article.Id})
return nil
}
blog.downloadAttachments(article.Id, content.Content)
content.Content = linkRegex.ReplaceAllString(content.Content, `href="/blog/$1"`)
content.Content = attachmentRegex.ReplaceAllString(content.Content, fmt.Sprintf(`$1="/static/blog/%s/$3"`, article.Id))
logbuch.Debug("Article loaded", logbuch.Fields{"id": article.Id})
} else {
content = existingArticle.LatestArticleContent
logbuch.Debug("Article up to date, skipping refreshing cache", logbuch.Fields{"id": article.Id})
}
return content
}
func (blog *Blog) downloadAttachments(id, content string) {
if _, err := os.Stat(filepath.Join(blogFileCache, id)); os.IsNotExist(err) {
if err := os.MkdirAll(filepath.Join(blogFileCache, id), 0755); err != nil {
logbuch.Error("Error creating article file cache directory", logbuch.Fields{"err": err, "id": id})
return
}
}
results := attachmentURLRegex.FindAllStringSubmatch(content, -1)
for _, attachment := range results {
if len(attachment) == 4 {
resp, err := http.Get(attachment[2] + attachment[3])
if err != nil {
logbuch.Error("Error downloading blog attachment", logbuch.Fields{"err": err, "id": id})
continue
}
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
logbuch.Error("Error reading blog attachment body", logbuch.Fields{"err": err, "id": id})
continue
}
if err := resp.Body.Close(); err != nil {
logbuch.Error("Error closing response body on attachment download", logbuch.Fields{"err": err, "id": id})
}
if err := ioutil.WriteFile(filepath.Join(blogFileCache, id, attachment[3]), data, 0755); err != nil {
logbuch.Error("Error saving blog attachment on disk", logbuch.Fields{"err": err, "id": id})
}
}
}
}
func (blog *Blog) setArticles(articles []emvi.Article) {
blog.articles = articles
blog.articleMap = make(map[string]emvi.Article)
blog.articlesYear = make(map[int][]emvi.Article)
for _, article := range articles {
if blog.articlesYear[article.Published.Year()] == nil {
blog.articlesYear[article.Published.Year()] = make([]emvi.Article, 0)
}
blog.articlesYear[article.Published.Year()] = append(blog.articlesYear[article.Published.Year()], article)
blog.articleMap[article.Id] = article
}
}
func (blog *Blog) refreshIfRequired() {
if blog.nextUpdate.Before(time.Now()) {
blog.cache.Clear()
blog.loadArticles()
}
}

17
content/home.json Normal file
View File

@@ -0,0 +1,17 @@
{
"path": {
"en": "/"
},
"sitemap": {
"priority": "1.0"
},
"content": {
"content": [
{"ref": "head"},
{
"tpl": "intro"
},
{"ref": "end"}
]
}
}

3
content/refs/end.json Normal file
View File

@@ -0,0 +1,3 @@
{
"tpl": "end"
}

11
content/refs/head.json Normal file
View File

@@ -0,0 +1,11 @@
{
"tpl": "head",
"copy": {
"en": {
"copyright": "Marvin Blum",
"author": "Marvin Blum",
"title": "Hi, I'm Marvin.",
"meta_description": "Hi, I'm Marvin, co-founder of Pirsch Analytics, software engineer, and open-source enthusiast."
}
}
}

18
dev.sh
View File

@@ -1,18 +0,0 @@
#!/bin/bash
# This file is for local development only!
# It configures and starts the website for local development.
# The "secret" for the Emvi API can be shared, as it gives access to public content only.
export MB_LOGLEVEL=debug
export MB_ALLOWED_ORIGINS=*
export MB_HOST=localhost:8080
export MB_HOT_RELOAD=true
export MB_EMVI_CLIENT_ID=3fBBn144yvSF9R3dPC8l
export MB_EMVI_CLIENT_SECRET=
export MB_EMVI_ORGA=marvin
export MB_PIRSCH_CLIENT_ID=gEb3pvgxZvZzFRlOTdMgPtyLvNYgeVKe
export MB_PIRSCH_CLIENT_SECRET=E7UqJehmxgnVuw81oq6ZhJAx9vCHqMimCUFfil7UFgbGhgQVVINqU7JqHBgaUvHg
export MB_PIRSCH_HOSTNAME=marvinblum.de
go run main.go

39
dev_config.json Normal file
View File

@@ -0,0 +1,39 @@
{
"dev": true,
"server": {
"host": "localhost",
"port": 8080,
"shutdown_time": 30,
"write_timeout": 5,
"read_timeout": 5,
"hostname": "marvinblum.de",
"secure_cookies": false,
"cookie_domain_name": "marvinblum.de"
},
"content": {
"provider": "git",
"update_seconds": 30,
"repository": "https://github.com/Kugelschieber/marvinblum.git"
},
"cors": {
"origins": "*",
"loglevel": "info"
},
"sass": {
"entrypoint": "main.scss",
"dir": "assets/scss",
"watch": true,
"out": "static/web/main.css",
"out_source_map": "static/web/main.css.map"
},
"js": {
"entrypoint": "main.js",
"dir": "assets/js",
"watch": true,
"out": "static/web/main.min.js"
},
"analytics": {
"provider": "pirsch",
"client_secret": ""
}
}

View File

@@ -1,75 +0,0 @@
version: "3"
services:
traefik:
image: "traefik:v2.3"
container_name: traefik
restart: always
networks:
- traefik-internal
command:
- "--api.dashboard=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--providers.docker.network=marvinblum_traefik-internal"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.tls-resolver.acme.httpchallenge=true"
- "--certificatesresolvers.tls-resolver.acme.httpchallenge.entrypoint=web"
- "--certificatesresolvers.tls-resolver.acme.email=marvin@marvinblum.de"
- "--certificatesresolvers.tls-resolver.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80"
- "443:443"
- "8080:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- /root/marvinblum/letsencrypt:/letsencrypt
labels:
- "traefik.enable=true"
- "traefik.port=9999"
- "traefik.http.routers.traefik.entrypoints=websecure"
- "traefik.http.routers.traefik.rule=Host(`traefik.marvinblum.de`)"
- "traefik.http.routers.traefik.service=api@internal"
- "traefik.http.routers.traefik.tls.certresolver=tls-resolver"
- "traefik.http.routers.traefik.middlewares=traefik-auth"
- "traefik.http.middlewares.traefik-auth.basicauth.users=marvinblum:$$apr1$$u.IJozER$$DoY0zwzgAciDpPs4vZvxY/"
# Global redirection: http to https
- 'traefik.http.routers.http-catchall.rule=HostRegexp(`{host:(www\.)?.+}`)'
- "traefik.http.routers.http-catchall.entrypoints=web"
- "traefik.http.routers.http-catchall.middlewares=wwwtohttps"
# Global redirection: https (www.) to https
- 'traefik.http.routers.wwwsecure-catchall.rule=HostRegexp(`{host:(www\.).+}`)'
- "traefik.http.routers.wwwsecure-catchall.entrypoints=websecure"
- "traefik.http.routers.wwwsecure-catchall.tls=true"
- "traefik.http.routers.wwwsecure-catchall.middlewares=wwwtohttps"
# middleware: http(s)://(www.) to https://
- 'traefik.http.middlewares.wwwtohttps.redirectregex.regex=^https?://(?:www\.)?(.+)'
- 'traefik.http.middlewares.wwwtohttps.redirectregex.replacement=https://$${1}'
- 'traefik.http.middlewares.wwwtohttps.redirectregex.permanent=true'
marvinblum:
image: kugel/marvinblum
container_name: marvinblum
restart: always
depends_on:
- traefik
networks:
- traefik-internal
env_file:
- secrets.env
environment:
MB_EMVI_CLIENT_ID: 3fBBn144yvSF9R3dPC8l
MB_EMVI_ORGA: marvin
MB_PIRSCH_CLIENT_ID: mkiAzI2ZGjGBv8fpwh1A09fCJ8G1YFgx
MB_PIRSCH_HOSTNAME: marvinblum.de
labels:
- "traefik.enable=true"
- "traefik.port=8888"
- "traefik.http.routers.marvinblum.rule=Host(`marvinblum.de`) || Host(`www.marvinblum.de`)"
- "traefik.http.routers.marvinblum.entrypoints=websecure"
- "traefik.http.routers.marvinblum.tls=true"
- "traefik.http.routers.marvinblum.tls.certresolver=tls-resolver"
networks:
traefik-internal:
driver: bridge

18
go.mod
View File

@@ -1,18 +0,0 @@
module github.com/Kugelschieber/marvinblum
go 1.15
require (
github.com/NYTimes/gziphandler v1.1.1
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emvi/api-go v0.2.2
github.com/emvi/logbuch v1.1.1
github.com/gorilla/mux v1.8.0
github.com/gosimple/slug v1.9.0
github.com/kr/pretty v0.1.0 // indirect
github.com/lib/pq v1.9.0
github.com/pirsch-analytics/pirsch-go-sdk v0.0.0-20201215183417-0e2a519a0dd1
github.com/rs/cors v1.7.0
github.com/stretchr/testify v1.6.1 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
)

33
go.sum
View File

@@ -1,33 +0,0 @@
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emvi/api-go v0.2.2 h1:NrZNl0o0xAbgfK1dFsRt/BKLesHLVjC4OKXHKTZsIis=
github.com/emvi/api-go v0.2.2/go.mod h1:g9RdDC3s5ebCknAHQQ5PjoM2vRFSyyGoOUX3QkDKU+o=
github.com/emvi/logbuch v1.1.1/go.mod h1:J2Wgbr3BuSc1JO+D2MBVh6q3WPVSK5GzktwWz8pvkKw=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gosimple/slug v1.9.0 h1:r5vDcYrFz9BmfIAMC829un9hq7hKM4cHUrsv36LbEqs=
github.com/gosimple/slug v1.9.0/go.mod h1:AMZ+sOVe65uByN3kgEyf9WEBKBCSS+dJjMX9x4vDJbg=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8=
github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/pirsch-analytics/pirsch-go-sdk v0.0.0-20201215183417-0e2a519a0dd1 h1:yn6F902YFyEMfXQIARsTXLW42TQKuH+BrKk0MjQu/ps=
github.com/pirsch-analytics/pirsch-go-sdk v0.0.0-20201215183417-0e2a519a0dd1/go.mod h1:PF2vnJw8FYcXQe6OTPQQcGn8l/agkpl7T4YO9d2aPSE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ=
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q=
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

201
main.go
View File

@@ -1,201 +0,0 @@
package main
import (
"context"
"github.com/Kugelschieber/marvinblum/blog"
"github.com/Kugelschieber/marvinblum/tpl"
"github.com/NYTimes/gziphandler"
emvi "github.com/emvi/api-go"
"github.com/emvi/logbuch"
"github.com/gorilla/mux"
_ "github.com/lib/pq"
"github.com/pirsch-analytics/pirsch-go-sdk"
"github.com/rs/cors"
"html/template"
"net/http"
"os"
"os/signal"
"strings"
"time"
)
const (
staticDir = "static"
staticDirPrefix = "/static/"
logTimeFormat = "2006-01-02_15:04:05"
envPrefix = "MB_"
shutdownTimeout = time.Second * 30
)
var (
client *pirsch.Client
tplCache *tpl.Cache
blogInstance *blog.Blog
)
func serveAbout() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
go hit(r)
tplCache.Render(w, "about.html", struct {
Articles []emvi.Article
}{
blogInstance.GetLatestArticles(),
})
}
}
func serveLegal() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
go hit(r)
tplCache.Render(w, "legal.html", nil)
}
}
func serveBlogPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
go hit(r)
tplCache.Render(w, "blog.html", struct {
Articles map[int][]emvi.Article
}{
blogInstance.GetArticles(),
})
}
}
func serveBlogArticle() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
slug := strings.Split(vars["slug"], "-")
if len(slug) == 0 {
http.Redirect(w, r, "/notfound", http.StatusFound)
return
}
article := blogInstance.GetArticle(slug[len(slug)-1])
if len(article.Id) == 0 {
http.Redirect(w, r, "/notfound", http.StatusFound)
return
}
// track the hit if the article was found, otherwise we don't care
go hit(r)
tplCache.RenderWithoutCache(w, "article.html", struct {
Title string
Content template.HTML
Published time.Time
}{
article.LatestArticleContent.Title,
template.HTML(article.LatestArticleContent.Content),
article.Published,
})
}
}
func serveTracking() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "https://marvinblum.pirsch.io/", http.StatusFound)
}
}
func serveNotFound() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
tplCache.Render(w, "notfound.html", nil)
}
}
func setupRouter() *mux.Router {
router := mux.NewRouter()
router.PathPrefix(staticDirPrefix).Handler(http.StripPrefix(staticDirPrefix, gziphandler.GzipHandler(http.FileServer(http.Dir(staticDir)))))
router.Handle("/blog/{slug}", serveBlogArticle())
router.Handle("/blog", serveBlogPage())
router.Handle("/legal", serveLegal())
router.Handle("/tracking", serveTracking())
router.Handle("/", serveAbout())
router.NotFoundHandler = serveNotFound()
return router
}
func configureLog() {
logbuch.SetFormatter(logbuch.NewFieldFormatter(logTimeFormat, "\t\t"))
logbuch.Info("Configure logging...")
level := strings.ToLower(os.Getenv("MB_LOGLEVEL"))
if level == "debug" {
logbuch.SetLevel(logbuch.LevelDebug)
} else if level == "info" {
logbuch.SetLevel(logbuch.LevelInfo)
} else {
logbuch.SetLevel(logbuch.LevelWarning)
}
}
func logEnvConfig() {
for _, e := range os.Environ() {
if strings.HasPrefix(e, envPrefix) {
pair := strings.Split(e, "=")
logbuch.Info(pair[0] + "=" + pair[1])
}
}
}
func configureCors(router *mux.Router) http.Handler {
logbuch.Info("Configuring CORS...")
origins := strings.Split(os.Getenv("MB_ALLOWED_ORIGINS"), ",")
c := cors.New(cors.Options{
AllowedOrigins: origins,
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"},
AllowedHeaders: []string{"*"},
AllowCredentials: true,
Debug: strings.ToLower(os.Getenv("MB_CORS_LOGLEVEL")) == "debug",
})
return c.Handler(router)
}
func start(handler http.Handler) {
logbuch.Info("Starting server...")
var server http.Server
server.Handler = handler
server.Addr = os.Getenv("MB_HOST")
go func() {
sigint := make(chan os.Signal)
signal.Notify(sigint, os.Interrupt)
<-sigint
logbuch.Info("Shutting down server...")
ctx, _ := context.WithTimeout(context.Background(), shutdownTimeout)
if err := server.Shutdown(ctx); err != nil {
logbuch.Fatal("Error shutting down server gracefully", logbuch.Fields{"err": err})
}
}()
if err := server.ListenAndServe(); err != http.ErrServerClosed {
logbuch.Fatal("Error starting server", logbuch.Fields{"err": err})
}
logbuch.Info("Server shut down")
}
func hit(r *http.Request) {
if err := client.Hit(r); err != nil {
logbuch.Warn("Error sending page hit to pirsch", logbuch.Fields{"err": err})
}
}
func main() {
configureLog()
logEnvConfig()
client = pirsch.NewClient(os.Getenv("MB_PIRSCH_CLIENT_ID"),
os.Getenv("MB_PIRSCH_CLIENT_SECRET"),
os.Getenv("MB_PIRSCH_HOSTNAME"),
nil)
tplCache = tpl.NewCache()
blogInstance = blog.NewBlog(tplCache)
router := setupRouter()
corsConfig := configureCors(router)
start(corsConfig)
}

View File

@@ -1,2 +0,0 @@
MB_EMVI_CLIENT_SECRET=
MB_PIRSCH_CLIENT_SECRET=

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -1,281 +0,0 @@
/*! concrete.css v1.3.0 | MIT License | github.com/louismerlin/concrete.css/ */
/* The Basics */
html {
font-size: 62.5%;
box-sizing: border-box;
}
body {
font-size: 2rem;
font-weight: 400;
background: white;
color: #121212;
font-family: Helvetica, Arial, sans-serif;
}
*, ::after, ::before {
box-sizing: inherit;
}
a {
color: #121212;
}
blockquote, dl, figure, form, ol, p, pre, table, ul {
margin-bottom: 2.2rem;
}
img {
height: auto;
max-width: 100%;
}
/* A Cool Container */
.container {
margin: 0 auto;
max-width: 66rem;
padding: 0 1rem;
width: 100%;
position: relative;
}
/* The Button */
.button {
text-decoration: none;
}
button,
.button,
input[type="button"],
input[type="reset"],
input[type="submit"] {
display: inline-block;
vertical-align: middle;
border-radius: 0;
background: white;
line-height: 2.2rem;
font-size: 1.6rem;
color: #121212;
border: 0.3rem solid #121212;
padding: 0.4rem 1rem;
cursor: pointer;
}
button.filled,
.button.filled,
input[type="button"].filled,
input[type="reset"].filled,
input[type="submit"].filled {
color: white;
background: #121212;
}
.button,
button,
dd,
dt,
li,
input[type="button"],
input[type="reset"],
input[type="submit"] {
margin-bottom: 1.0rem;
}
/* The List */
ul {
list-style: square;
}
/* The Form */
fieldset {
border-width: 0;
padding: 0;
}
label, legend {
display: block;
font-weight: bold;
margin-bottom: .5rem;
}
input[type="email"],
input[type="number"],
input[type="password"],
input[type="search"],
input[type="tel"],
input[type="text"],
input[type="url"],
textarea,
select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-color: transparent;
border: 0.1rem solid #121212;
border-radius: 0;
box-shadow: none;
box-sizing: inherit;
padding: .6rem 1.0rem;
width: 100%;
}
fieldset, input, select, textarea {
margin-bottom: 1.5rem;
}
/* The Table */
table {
width: 100%;
border-spacing: 0;
}
td, th {
padding: 0.6rem 0;
}
td {
border-bottom: 0.1rem solid #121212;
}
th {
border-bottom: 0.3rem solid #121212;
text-align: left;
}
/* The Blockquote and the Code */
blockquote, pre {
border-left: 0.3rem solid #121212;
margin-left: 0;
margin-right: 0;
padding: 1rem 1.5rem;
overflow-y: hidden;
}
pre {
border: 0.1rem dotted #121212;
border-left: 0.3rem solid #121212;
}
pre > code {
padding: 1rem 1.4rem;
font-size: 1.6rem;
white-space: pre;
display: block;
}
/* The Progress Bar */
progress {
-moz-appearance: none;
-webkit-appearance: none;
border-radius: 0;
display: block;
height: 1rem;
border: 0.1rem solid #121212;
background: white;
color: #121212;
overflow: hidden;
padding: 0;
width: 100%;
}
progress::-webkit-progress-bar {
background-color: white;
}
progress::-webkit-progress-value {
background-color: #121212;
}
progress::-moz-progress-bar {
background-color: #121212;
}
progress::-ms-fill {
background-color: #121212;
}
/* The Break Line */
hr {
border: 0.2rem solid #121212;
border-bottom-width: 0.1rem;
}
/* Dark Mode */
@media (prefers-color-scheme: dark) {
body {
background: #121212;
color: white;
}
a {
color: white;
}
button,
.button,
input[type="button"],
input[type="reset"],
input[type="submit"] {
background: #121212;
color: white;
border-color: white;
}
button.filled,
.button.filled,
input[type="button"].filled,
input[type="reset"].filled,
input[type="submit"].filled {
color: #121212;
background: white;
}
input[type="email"],
input[type="number"],
input[type="password"],
input[type="search"],
input[type="tel"],
input[type="text"],
input[type="url"],
textarea,
select {
color: white;
border-color: white;
}
td {
border-bottom-color: white;
}
th {
border-bottom-color: white;
}
blockquote, pre {
border-left-color: white;
}
pre {
border-color: white;
border-left-color: white;
}
progress {
border-color: white;
background: #121212;
color: white;
}
progress::-webkit-progress-bar {
background-color: #121212;
}
progress::-webkit-progress-value {
background-color: white;
}
progress::-moz-progress-bar {
background-color: white;
}
progress::-ms-fill {
background-color: white;
}
hr {
border-color: white;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#da532c</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 567 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

View File

@@ -1,218 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="460.000000pt" height="460.000000pt" viewBox="0 0 460.000000 460.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,460.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M2000 4199 c-80 -8 -163 -31 -185 -50 -5 -5 0 -9 14 -9 36 0 14 -18
-32 -25 -23 -4 -59 -18 -79 -31 -21 -13 -42 -24 -47 -24 -6 0 -16 -9 -24 -20
-7 -10 -32 -23 -56 -29 -48 -10 -91 -45 -91 -72 0 -10 -9 -21 -20 -24 -11 -3
-20 -15 -20 -26 0 -10 -6 -19 -13 -19 -15 0 -65 -40 -51 -40 5 0 -8 -11 -28
-23 l-37 -23 19 -29 c13 -20 16 -34 9 -46 -5 -10 -9 -27 -9 -38 0 -12 -4 -21
-10 -21 -5 0 -10 -7 -10 -17 0 -15 1 -15 18 0 10 10 24 17 31 17 17 0 -9 -46
-53 -93 -20 -21 -37 -48 -37 -60 -1 -12 -2 -33 -3 -47 -1 -14 -7 -25 -13 -25
-7 0 -13 -4 -12 -9 0 -5 -3 -12 -8 -15 -5 -3 -19 -23 -32 -43 -29 -49 -17 -70
15 -27 25 34 32 33 14 -2 -8 -13 -7 -19 0 -19 7 0 3 -12 -7 -27 -28 -41 -44
-83 -33 -83 9 0 4 -145 -5 -161 -13 -20 30 -293 45 -284 3 2 6 -15 7 -38 1
-23 4 -82 5 -132 2 -49 7 -97 10 -105 3 -8 14 -44 23 -80 19 -70 30 -90 51
-90 11 0 14 17 14 70 0 79 20 210 32 210 4 0 8 -5 8 -11 0 -27 75 -168 107
-201 90 -92 268 -126 391 -74 34 15 83 42 108 61 25 19 50 33 55 29 5 -3 9 0
9 7 0 7 3 10 6 6 8 -8 42 23 64 58 43 71 91 271 72 302 -4 6 -18 14 -31 18
-26 8 -81 59 -81 75 0 6 -5 10 -12 10 -6 0 -9 2 -6 5 7 7 -58 75 -72 75 -6 0
-22 7 -35 16 -14 9 -51 20 -82 24 -32 5 -99 14 -148 21 -101 14 -141 4 -160
-39 -5 -12 -17 -22 -27 -22 -25 0 -49 -11 -43 -20 3 -5 15 -6 28 -3 19 4 18 2
-5 -12 -16 -8 -28 -20 -28 -25 0 -6 -7 -10 -16 -10 -8 0 -13 4 -9 9 3 5 1 12
-4 15 -5 4 -8 -2 -7 -11 3 -19 -28 -42 -56 -43 -10 0 -18 6 -18 13 -4 122 -2
238 3 244 12 11 8 23 -7 23 -19 0 -29 30 -15 47 7 7 9 16 5 20 -3 3 2 15 11
25 14 15 15 21 4 27 -10 7 -10 13 -2 29 9 16 8 24 -4 36 -8 8 -11 17 -6 20 8
6 25 64 22 79 -1 5 2 6 7 3 4 -2 13 4 20 15 7 10 8 19 3 19 -6 0 -3 15 6 33
77 154 59 139 173 141 74 2 122 20 143 53 5 8 12 11 18 8 5 -4 9 -2 9 3 0 5
14 9 32 10 18 0 50 8 73 18 92 38 112 45 163 54 30 5 65 19 78 29 14 11 32 17
40 14 8 -3 14 -1 14 4 0 10 57 18 201 29 37 2 77 7 90 10 46 11 285 13 334 3
28 -6 61 -12 75 -15 93 -19 219 -123 268 -221 9 -18 19 -33 23 -33 4 0 10 -11
13 -24 11 -43 106 -121 106 -86 0 6 -4 10 -10 10 -15 0 -12 40 3 41 6 1 22 0
34 0 12 -1 26 5 31 15 12 22 4 54 -14 54 -9 0 -14 11 -14 30 0 17 -6 33 -14
36 -8 3 -17 16 -20 29 -3 13 -18 30 -34 37 -16 8 -26 21 -25 32 1 13 -2 15
-10 7 -18 -18 -27 -13 -27 15 0 14 -4 23 -9 19 -5 -3 -27 15 -48 40 -20 25
-43 43 -50 41 -6 -3 -27 13 -45 34 -18 21 -49 57 -68 79 -19 23 -39 41 -44 41
-5 0 -20 10 -33 22 -19 17 -23 19 -17 5 9 -24 -3 -21 -38 8 -43 36 -132 54
-371 75 -163 14 -292 32 -297 41 -9 14 -89 17 -180 8z m319 -51 c11 -11 12
-10 6 0 -6 11 -3 12 11 4 11 -5 47 -12 82 -16 35 -4 73 -11 85 -16 18 -7 14
-9 -20 -9 -30 -1 -43 -5 -43 -15 0 -8 -6 -17 -12 -19 -7 -3 25 -6 72 -7 141
-2 186 -10 141 -25 -12 -3 -21 -14 -20 -23 0 -11 3 -12 6 -4 7 17 23 15 23 -2
0 -10 -16 -17 -47 -21 -27 -3 -82 -10 -124 -16 -139 -20 -288 3 -264 40 6 11
-4 14 -35 12 -10 0 -11 1 -2 5 6 3 10 10 6 15 -4 8 69 15 117 11 12 -1 16 6
15 23 -1 23 -16 37 -16 15 0 -5 -7 -10 -15 -10 -8 0 -15 6 -15 14 0 16 -80 33
-99 21 -6 -3 -11 -2 -11 2 0 5 -12 8 -27 7 -16 0 -37 2 -48 5 -11 3 25 9 80
12 55 3 109 6 121 7 11 1 26 -4 33 -10z m291 -44 c0 -2 -9 -4 -20 -4 -11 0
-20 4 -20 9 0 5 9 7 20 4 11 -3 20 -7 20 -9z m-362 -1 c-10 -2 -26 -2 -35 0
-10 3 -2 5 17 5 19 0 27 -2 18 -5z m672 -88 c0 -5 -5 -3 -10 5 -5 8 -10 20
-10 25 0 6 5 3 10 -5 5 -8 10 -19 10 -25z m-230 5 c0 -5 -7 -10 -15 -10 -8 0
-15 5 -15 10 0 6 7 10 15 10 8 0 15 -4 15 -10z m-933 -1134 c-7 -17 22 -25
108 -33 76 -6 88 -17 40 -38 -31 -12 -40 -13 -47 -3 -4 8 -8 8 -8 2 0 -6 -36
-16 -81 -23 -65 -10 -86 -10 -110 1 -16 7 -29 16 -29 20 0 15 43 46 80 57 22
6 40 16 40 21 0 6 3 10 6 10 3 0 4 -6 1 -14z m-273 -72 l-49 -46 -3 -81 c-2
-45 1 -93 7 -107 9 -24 10 -24 11 10 5 81 11 106 32 122 22 16 23 15 45 -24
13 -22 23 -49 23 -59 0 -11 8 -20 18 -21 9 0 24 -1 32 -2 8 -1 16 -9 18 -19 2
-9 8 -17 14 -17 6 0 23 -14 38 -30 24 -26 34 -30 85 -31 45 -1 59 -5 62 -19 6
-22 140 -55 190 -45 l35 6 -21 -29 c-59 -84 -148 -126 -266 -126 -104 0 -168
25 -235 93 -67 68 -101 145 -108 249 -6 90 14 149 66 192 56 46 60 36 6 -16z
m-217 -36 c-3 -8 -6 -5 -6 6 -1 11 2 17 5 13 3 -3 4 -12 1 -19z m620 6 c12 -3
24 -12 27 -19 4 -11 -11 -14 -72 -12 -42 0 -88 -3 -102 -7 -22 -8 -23 -7 -11
8 23 28 106 44 158 30z m23 -114 c0 -5 -4 -10 -10 -10 -5 0 -10 5 -10 10 0 6
5 10 10 10 6 0 10 -4 10 -10z"/>
<path d="M2448 4193 c7 -3 16 -2 19 1 4 3 -2 6 -13 5 -11 0 -14 -3 -6 -6z"/>
<path d="M2523 4184 c3 -3 33 -12 67 -19 70 -15 72 -7 2 12 -52 13 -79 16 -69
7z"/>
<path d="M2674 4146 c23 -18 36 -20 36 -6 0 6 -4 10 -9 10 -5 0 -18 3 -28 6
-16 5 -16 4 1 -10z"/>
<path d="M2702 2976 c-19 -5 -40 -21 -50 -38 -9 -15 -24 -28 -32 -28 -9 0 -29
-13 -45 -30 -16 -16 -33 -30 -38 -30 -5 0 -37 -21 -70 -46 -50 -37 -65 -44
-86 -38 -34 10 -101 11 -101 2 0 -5 1 -8 3 -9 1 0 25 -6 52 -13 36 -9 53 -20
62 -37 14 -31 23 -109 12 -109 -5 0 -9 5 -9 12 0 6 -3 9 -6 5 -3 -3 -3 -19 1
-36 4 -21 13 -31 25 -31 10 0 22 -9 25 -20 9 -28 71 -119 107 -157 17 -17 58
-44 92 -59 52 -24 74 -29 146 -29 47 0 105 6 129 13 66 20 43 25 -30 6 -116
-30 -270 8 -341 83 -30 32 -103 171 -94 180 3 4 6 0 6 -7 0 -11 3 -11 11 -3
15 15 3 47 -16 40 -20 -8 -31 74 -16 124 30 101 200 177 411 184 91 3 112 1
159 -18 121 -49 167 -141 143 -284 -6 -37 -10 -68 -8 -70 2 -2 15 41 29 94 17
61 34 103 46 113 11 8 21 30 23 50 3 35 3 35 -57 51 -68 18 -151 54 -160 69
-3 5 -15 10 -25 10 -10 0 -25 7 -34 15 -8 8 -23 15 -33 16 -10 0 -13 3 -5 6 6
2 12 8 12 13 0 11 -49 19 -55 9 -5 -7 -23 -6 -125 4 -14 2 -40 -1 -58 -7z"/>
<path d="M3294 2929 c2 -25 9 -56 16 -70 13 -23 12 -24 -7 -6 -20 17 -20 16
-22 -40 -1 -36 -7 -60 -15 -65 -11 -6 -11 -17 1 -65 8 -32 14 -69 14 -83 0
-21 16 -131 34 -242 2 -15 1 -29 -4 -32 -8 -5 -13 -96 -6 -96 9 0 55 52 55 61
0 6 -4 8 -9 5 -4 -3 0 25 11 62 21 73 23 117 7 205 -11 61 -6 82 17 73 8 -3
14 1 15 12 1 9 5 4 10 -13 8 -30 8 -26 -4 65 -1 8 -7 24 -14 35 -6 11 -12 25
-13 30 -2 6 -4 15 -5 20 -12 40 -12 53 0 49 7 -3 14 5 17 20 3 14 1 23 -3 20
-5 -3 -9 1 -9 9 0 8 -12 23 -27 33 -15 9 -35 27 -45 38 -16 20 -17 20 -14 -25z
m50 -181 c-12 -19 -44 13 -44 44 l0 32 25 -34 c13 -18 22 -37 19 -42z"/>
<path d="M2640 2794 c-19 -7 -43 -13 -53 -13 -10 -1 -20 -8 -23 -16 -4 -8 -13
-15 -21 -15 -8 0 -11 -4 -8 -10 3 -5 2 -10 -2 -10 -5 0 -9 -16 -9 -35 0 -32 6
-40 28 -36 8 1 -4 -36 -22 -70 -19 -35 13 -52 118 -63 29 -3 32 -1 32 25 0 27
1 28 49 24 46 -4 51 -2 80 31 17 19 31 45 31 58 0 12 3 26 6 29 11 10 64 -26
64 -43 0 -11 11 -15 40 -16 47 -2 51 9 13 33 -16 10 -36 25 -46 34 -17 16
-102 40 -147 43 -27 1 -53 36 -32 43 6 3 12 9 12 14 0 13 -69 9 -110 -7z m13
-131 c2 -10 8 -26 12 -35 5 -14 2 -18 -13 -18 -27 0 -35 15 -28 46 7 28 22 32
29 7z m151 -4 c-8 -14 -24 -10 -24 6 0 9 6 12 15 9 8 -4 12 -10 9 -15z m-137
-101 c-3 -8 -6 -5 -6 6 -1 11 2 17 5 13 3 -3 4 -12 1 -19z"/>
<path d="M3502 2556 c-1 -14 2 -26 7 -26 5 0 8 -11 7 -24 -2 -14 1 -30 6 -38
5 -8 8 5 6 35 -2 55 -22 96 -26 53z"/>
<path d="M1225 2520 c-8 -14 4 -110 14 -110 12 0 13 108 2 114 -5 4 -13 2 -16
-4z"/>
<path d="M3115 2471 c-7 -23 -88 -109 -125 -133 -21 -14 -22 -16 -4 -12 42 11
158 143 141 161 -3 2 -8 -5 -12 -16z"/>
<path d="M1304 2298 c3 -10 1 -18 -4 -18 -6 0 -10 -9 -10 -20 0 -13 7 -20 19
-20 23 0 36 -16 21 -25 -12 -7 -4 -25 12 -25 15 0 3 52 -23 92 -11 18 -19 25
-15 16z"/>
<path d="M1955 2130 c3 -5 11 -10 16 -10 6 0 7 5 4 10 -3 6 -11 10 -16 10 -6
0 -7 -4 -4 -10z"/>
<path d="M2063 2104 c-7 -3 -13 -14 -13 -25 0 -10 -4 -19 -10 -19 -20 0 -9
-21 18 -33 15 -6 31 -17 35 -24 12 -19 30 -16 45 7 7 11 21 20 32 20 26 0 25
13 -4 45 -24 25 -77 40 -103 29z"/>
<path d="M2416 2081 c-3 -5 13 -11 37 -13 40 -5 55 -3 46 6 -11 9 -78 16 -83
7z"/>
<path d="M2033 1926 c4 -10 7 -20 7 -22 0 -2 7 -4 15 -4 8 0 15 -4 15 -10 0
-5 5 -10 11 -10 16 0 6 45 -11 45 -8 0 -21 5 -29 9 -11 7 -13 5 -8 -8z"/>
<path d="M1840 1840 c0 -19 3 -21 12 -12 9 9 9 15 0 24 -9 9 -12 7 -12 -12z"/>
<path d="M1790 1835 c-10 -12 -10 -15 4 -15 9 0 16 7 16 15 0 8 -2 15 -4 15
-2 0 -9 -7 -16 -15z"/>
<path d="M1895 1840 c-3 -5 3 -10 15 -10 12 0 18 5 15 10 -3 6 -10 10 -15 10
-5 0 -12 -4 -15 -10z"/>
<path d="M2110 1782 c0 -5 -6 -9 -12 -9 -7 0 -37 -5 -67 -12 -29 -6 -72 -9
-96 -5 -33 4 -44 2 -49 -11 -3 -10 -11 -13 -18 -9 -12 7 25 -38 65 -79 15 -16
21 -15 18 5 0 3 12 0 27 -7 35 -18 131 -35 137 -25 3 5 14 6 25 3 15 -4 20 0
20 14 0 13 8 19 25 21 14 1 25 -3 25 -8 0 -7 8 -6 23 1 12 7 51 14 87 15 36 2
101 5 145 8 48 2 82 0 86 -6 8 -12 119 0 119 13 0 5 -8 9 -17 10 -16 0 -43 5
-70 10 -4 1 -16 -1 -27 -6 -10 -4 -60 -3 -110 4 -51 6 -99 9 -108 6 -21 -8
-90 24 -81 38 3 6 1 7 -6 3 -6 -4 -18 0 -27 8 -9 9 -19 14 -24 11 -5 -3 -11
-1 -15 5 -7 12 -75 14 -75 2z"/>
<path d="M1573 1643 c-21 -8 -15 -30 10 -37 21 -7 21 -7 2 -15 -27 -11 -45 -3
-38 17 5 13 3 14 -7 5 -7 -6 -19 -9 -26 -6 -19 7 -18 -5 2 -24 9 -10 13 -25
10 -39 -3 -12 -2 -26 4 -29 5 -3 10 -13 10 -21 0 -15 40 -106 50 -114 4 -3 15
-21 26 -40 10 -19 21 -37 25 -40 16 -15 77 -125 70 -128 -11 -4 -1 -45 13 -54
5 -4 12 -22 14 -40 1 -18 12 -40 23 -50 22 -19 6 -31 -23 -17 -13 7 -15 5 -11
-11 6 -22 11 -24 32 -11 9 6 19 4 28 -4 15 -16 17 -45 3 -45 -7 0 -7 -6 0 -20
14 -26 34 -26 27 0 -3 11 -1 18 4 14 5 -3 9 -11 9 -18 0 -7 11 -23 25 -36 14
-13 25 -20 25 -16 0 4 10 1 23 -6 l22 -12 -25 -6 -25 -6 27 -6 c15 -3 25 -1
22 4 -3 5 23 6 62 2 50 -5 65 -4 60 4 -10 16 28 16 57 0 14 -7 72 -11 157 -11
164 1 195 5 246 32 21 12 43 21 47 21 12 0 81 32 87 41 3 3 19 15 35 25 17 11
51 41 75 68 25 27 53 57 63 67 9 11 17 23 17 27 0 5 12 19 26 33 38 36 65 84
58 104 -6 14 -3 16 14 11 17 -6 21 -4 16 8 -4 10 -1 16 7 16 7 0 10 3 7 6 -4
3 -18 -2 -32 -12 -25 -16 -28 -16 -46 1 -11 10 -23 15 -27 12 -3 -4 -1 -7 5
-7 6 0 16 -9 22 -20 9 -17 8 -19 -7 -13 -10 4 -29 17 -42 29 l-25 23 -11 -22
c-8 -17 -21 -23 -53 -25 -23 -2 -42 -8 -42 -13 0 -6 4 -8 9 -4 5 3 12 1 15 -4
11 -17 -10 -21 -30 -6 -26 19 -34 19 -28 -2 5 -17 5 -17 -9 0 -17 20 -35 23
-33 5 0 -7 -3 -13 -9 -13 -5 0 -10 8 -9 18 1 23 -17 22 -23 -3 -3 -11 -9 -17
-14 -14 -5 3 -9 -2 -9 -10 0 -9 9 -21 20 -28 23 -15 27 -40 5 -32 -8 4 -15 2
-15 -4 0 -6 -13 -24 -29 -40 -18 -17 -26 -33 -21 -38 5 -5 -4 -9 -23 -9 -24 0
-38 -7 -54 -29 -13 -17 -23 -36 -23 -44 0 -8 -5 -10 -12 -5 -7 4 -34 6 -61 5
-31 -1 -49 2 -51 10 -2 7 -7 22 -12 33 -6 18 -8 18 -11 3 -6 -24 -25 -23 -53
2 -16 14 -26 17 -30 9 -4 -6 -10 4 -14 23 -5 26 -9 30 -16 18 -8 -13 -10 -12
-10 3 0 9 -6 17 -12 18 -7 0 -28 20 -46 45 -26 36 -30 49 -22 64 5 10 10 24
10 30 0 15 -43 30 -51 18 -3 -5 -9 -3 -12 6 -5 11 0 16 14 16 29 0 16 15 -36
39 -45 21 -55 37 -34 58 13 13 -23 53 -63 71 -27 11 -32 20 -29 52 1 11 -9 15
-40 14 -25 0 -43 4 -46 13 -3 7 -9 10 -14 7 -6 -3 -17 0 -26 8 -12 9 -15 9 -9
1 4 -7 2 -13 -4 -13 -6 0 -8 14 -4 39 6 39 6 40 -40 62 -45 21 -48 21 -60 4
-12 -16 -13 -16 -19 -1 -7 16 -13 18 -34 9z m10 -87 c4 -10 1 -13 -8 -9 -8 3
-12 9 -9 14 7 12 11 11 17 -5z m37 -22 c0 -14 -4 -23 -9 -20 -5 3 -7 15 -4 26
7 28 13 25 13 -6z m1206 -299 c2 -10 5 -29 9 -42 3 -13 1 -23 -4 -23 -13 0
-17 8 -25 54 -5 28 -4 37 5 34 7 -2 13 -13 15 -23z m57 -15 c3 6 4 5 3 -2 -4
-18 -28 -16 -34 2 -4 13 -3 13 10 2 11 -9 18 -9 21 -2z m-242 -48 c-7 -2 -18
1 -23 6 -8 8 -4 9 13 5 13 -4 18 -8 10 -11z"/>
<path d="M2985 1340 c3 -6 -1 -13 -10 -16 -12 -5 -13 -9 -4 -14 6 -5 17 -3 24
3 13 13 9 37 -7 37 -5 0 -6 -5 -3 -10z"/>
<path d="M3117 1283 c-3 -5 -7 -35 -11 -68 -4 -33 -9 -82 -12 -108 l-6 -49 30
16 c65 34 201 7 236 -46 8 -13 13 -38 11 -58 -2 -19 -4 -41 -4 -47 -1 -7 -12
-13 -25 -13 -15 0 -26 -7 -29 -17 -4 -14 -5 -12 -6 5 -1 14 5 22 15 22 9 0 14
6 11 13 -2 6 -11 11 -21 9 -9 -2 -16 4 -16 13 0 18 -16 20 -24 3 -3 -7 -5 -1
-3 14 3 17 -4 36 -20 54 -12 15 -23 24 -23 19 0 -4 -6 -3 -12 1 -7 5 -35 9
-63 9 l-50 0 -9 -83 c-6 -53 -5 -100 1 -127 11 -47 9 -154 -2 -200 -15 -57
-78 -151 -149 -220 -99 -98 -201 -187 -234 -205 -15 -8 -41 -24 -57 -36 -42
-28 -150 -79 -212 -99 -38 -12 -70 -14 -125 -10 -83 7 -186 32 -208 50 -8 7
-28 13 -45 13 -16 0 -43 6 -60 13 -27 11 -54 17 -96 22 -8 1 -23 10 -33 20
-10 9 -21 17 -26 17 -14 0 -148 138 -155 159 -3 11 -15 23 -26 26 -17 6 -26
35 -20 68 1 4 -6 10 -14 13 -8 4 -15 12 -15 20 0 8 -5 14 -11 14 -6 0 -9 7 -5
15 3 8 1 15 -4 15 -6 0 -10 6 -10 14 0 8 -5 16 -12 18 -6 2 -13 15 -14 29 -2
13 -9 53 -15 89 -8 44 -10 129 -5 273 4 141 3 206 -4 202 -5 -3 -10 -12 -10
-19 0 -8 -6 -19 -13 -25 -14 -11 -37 -76 -43 -121 -5 -42 -6 -224 0 -240 5
-14 15 -90 22 -155 1 -16 3 -54 3 -83 1 -53 2 -54 85 -140 108 -112 178 -198
205 -250 12 -23 20 -42 17 -42 -5 0 -72 27 -111 45 -17 7 -51 23 -78 34 -26
11 -47 22 -47 24 0 2 26 2 58 1 l57 -3 -65 15 c-63 15 -132 42 -138 55 -1 3
-16 21 -32 39 -17 19 -30 40 -30 48 0 8 -12 29 -27 47 -16 18 -40 48 -56 66
-15 19 -36 45 -47 58 -11 13 -20 29 -20 36 0 7 -7 25 -15 42 -26 52 -14 220
16 239 5 3 9 12 9 20 0 14 7 28 89 193 23 46 40 85 38 88 -9 8 -101 -24 -167
-58 -74 -38 -197 -154 -254 -238 -20 -29 -36 -57 -36 -61 0 -4 -25 -24 -55
-45 -59 -39 -85 -77 -85 -123 0 -28 -6 -32 -107 -77 -60 -26 -171 -75 -248
-110 -77 -35 -180 -78 -230 -95 -50 -17 -117 -45 -150 -63 l-60 -32 -3 -117
-3 -118 625 0 624 0 16 35 c24 51 36 52 36 5 0 -29 4 -40 13 -37 6 2 12 16 11
30 -2 37 25 35 39 -3 l11 -30 613 0 c653 0 638 -1 696 50 16 14 57 45 91 70
34 25 90 65 124 90 34 25 85 61 113 81 27 20 98 81 156 135 124 115 219 179
312 209 21 6 20 5 -4 -32 -13 -21 -29 -43 -35 -50 -5 -7 -10 -15 -10 -19 0 -3
-9 -18 -20 -34 -11 -15 -16 -31 -12 -36 4 -5 3 -6 -2 -2 -8 7 -46 -32 -46 -47
0 -3 -13 -20 -30 -38 -16 -18 -30 -35 -30 -39 0 -4 -9 -10 -20 -13 -11 -3 -20
-12 -20 -19 0 -7 -11 -18 -25 -24 -14 -6 -25 -15 -25 -20 0 -6 -54 -51 -119
-97 -8 -5 -31 -27 -53 -48 -40 -39 -110 -71 -140 -63 -10 2 -18 0 -18 -6 0 -5
9 -12 20 -15 11 -3 18 -9 15 -13 -3 -5 -15 -6 -27 -2 -14 3 -19 2 -14 -6 5 -9
228 -12 872 -12 l864 0 0 123 0 122 -61 27 c-55 25 -61 30 -54 50 4 13 10 42
12 65 6 49 0 58 -114 191 -68 79 -106 132 -159 218 -39 64 -186 240 -236 282
-29 24 -75 55 -102 69 -65 33 -194 56 -271 48 -55 -6 -75 -1 -211 44 -121 41
-164 51 -217 51 -36 0 -68 -3 -70 -7z m253 -413 c0 -5 -7 -7 -15 -4 -11 5 -13
2 -9 -10 4 -11 1 -16 -10 -16 -14 0 -15 4 -6 20 11 20 40 28 40 10z"/>
<path d="M2450 1085 c0 -8 5 -15 10 -15 6 0 10 7 10 15 0 8 -4 15 -10 15 -5 0
-10 -7 -10 -15z"/>
<path d="M2705 61 c-3 -6 4 -9 15 -8 11 1 20 5 20 9 0 11 -28 10 -35 -1z"/>
<path d="M2760 60 c0 -5 5 -10 10 -10 6 0 10 5 10 10 0 6 -4 10 -10 10 -5 0
-10 -4 -10 -10z"/>
<path d="M2790 60 c0 -5 7 -10 16 -10 8 0 12 5 9 10 -3 6 -10 10 -16 10 -5 0
-9 -4 -9 -10z"/>
<path d="M2835 50 c3 -5 10 -10 16 -10 5 0 9 5 9 10 0 6 -7 10 -16 10 -8 0
-12 -4 -9 -10z"/>
<path d="M2835 10 c3 -5 8 -10 11 -10 2 0 4 5 4 10 0 6 -5 10 -11 10 -5 0 -7
-4 -4 -10z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -1,19 +0,0 @@
{
"name": "",
"short_name": "",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-384x384.png",
"sizes": "384x384",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
static/img/me.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

File diff suppressed because one or more lines are too long

349
static/normalize.css vendored
View File

@@ -1,349 +0,0 @@
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in iOS.
*/
html {
line-height: 1.15; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers.
*/
body {
margin: 0;
}
/**
* Render the `main` element consistently in IE.
*/
main {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* Remove the gray background on active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Remove the border on images inside links in IE 10.
*/
img {
border-style: none;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input { /* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select { /* 1 */
text-transform: none;
}
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline;
}
/**
* Remove the default vertical scrollbar in IE 10+.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10.
* 2. Remove the padding in IE 10.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in Edge, IE 10+, and Firefox.
*/
details {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Misc
========================================================================== */
/**
* Add the correct display in IE 10+.
*/
template {
display: none;
}
/**
* Add the correct display in IE 10.
*/
[hidden] {
display: none;
}

View File

@@ -1,138 +0,0 @@
html, body {
margin: 0;
padding: 0;
}
body {
max-width: 700px;
padding: 100px 20px 400px 20px;
margin: 0 auto;
}
.title {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.title div {
flex-grow: 1;
}
.title h1 {
font-size: 48px;
margin-bottom: 10px;
line-height: 1;
}
.title h2 {
font-weight: normal;
margin-top: 0;
line-height: 1;
}
.title img {
max-width: 100px;
max-height: 100px;
border-radius: 100%;
display: block;
margin-left: 20px;
}
.menu {
display: flex;
}
.menu .button {
margin-right: 10px;
}
.tracking {
width: 100%;
height: 300px;
}
.tracking-form {
display: flex;
align-items: center;
}
.tracking-form input {
margin: 3px 6px 3px 0;
}
.embed {
display: flex;
justify-content: center;
align-items: center;
text-decoration: none;
border: 1px dotted #121212;
padding: 1rem 1.5rem;
}
.embed .image {
max-width: 200px;
max-height: 200px;
}
.embed .info {
padding: 1rem 1.5rem;
}
.embed .info .title {
font-weight: bold;
margin: 0 0 10px 0;
}
.embed .info .url {
margin: 10px 0 0 0;
}
.break-line-anywhere {
line-break: anywhere;
}
input {
padding: 2px;
}
section {
margin: 80px 0;
}
p {
line-height: 1.5;
}
img {
display: block;
margin-left: auto;
margin-right: auto;
}
@media (max-width: 700px) {
body {
padding: 40px 20px 80px 20px;
}
.title h1 {
font-size: 36px;
}
.title h2 {
font-size: 24px;
}
.title img {
max-width: 72px;
max-height: 72px;
}
.menu {
display: block;
}
.tracking-form {
display: block;
}
}

1
static/web/main.css Normal file
View File

@@ -0,0 +1 @@
@font-face{font-family:"Open Sans";font-style:italic;font-weight:normal;font-stretch:100%;font-display:swap;src:url("../font/OpenSans-Italic.ttf") format("truetype")}@font-face{font-family:"Open Sans";font-style:normal;font-weight:normal;font-stretch:100%;font-display:swap;src:url("../font/OpenSans-Regular.ttf") format("truetype")}@font-face{font-family:"Open Sans";font-style:normal;font-weight:bold;font-stretch:100%;font-display:swap;src:url("../font/OpenSans-Bold.ttf") format("truetype")}@font-face{font-family:"Open Sans";font-style:italic;font-weight:bold;font-stretch:100%;font-display:swap;src:url("../font/OpenSans-BoldItalic.ttf") format("truetype")}*{box-sizing:border-box;font-family:"Open Sans",sans-serif;font-size:18px;line-height:1.5;color:#0f0f0f}body{padding:80px 40px;max-width:1000px;margin:0 auto}h1,h2{font-size:32px;margin:0 0 40px 0;font-weight:normal}h1 a,h2 a{font-size:32px}h2{font-size:26px}a{text-decoration:none;transition:all .3s;color:#82b3db}a:hover{color:#0f0f0f}nav{display:flex;justify-content:space-between;align-items:center;margin:0 0 80px 0}nav ul{display:flex;gap:20px;margin:0;padding:0}nav ul li{list-style:none;margin:0;padding:0}nav ul li a{color:#0f0f0f}nav ul li a:hover{color:#82b3db}nav .contact{display:inline-block;padding:8px 16px;border-radius:8px;border:2px solid #82b3db;color:#0f0f0f;transition:all .3s;color:#82b3db}nav .contact:hover{background:#82b3db;color:#fff}section{margin:0 0 80px 0}footer{margin:0 0 80px 0;padding:40px 0;border-style:solid;border-width:2px 0 0 0;border-color:#e6e6e6}.intro article{display:flex;gap:40px;justify-content:flex-start;align-items:center}.intro article h1{margin:0}.intro article img{max-width:256px;max-height:256px;aspect-ratio:1;border-radius:8px}/*# sourceMappingURL=main.css.map */

1
static/web/main.css.map Normal file
View File

@@ -0,0 +1 @@
{"version":3,"sourceRoot":"","sources":["../../assets/scss/_font.scss","../../assets/scss/main.scss"],"names":[],"mappings":"AAAA,WACI,wBACA,kBACA,mBACA,kBACA,kBACA,0DAGJ,WACI,wBACA,kBACA,mBACA,kBACA,kBACA,2DAGJ,WACI,wBACA,kBACA,iBACA,kBACA,kBACA,wDAGJ,WACI,wBACA,kBACA,iBACA,kBACA,kBACA,8DC1BJ,EACC,sBACA,mCACA,eACA,gBACA,MATW,QAYZ,KACC,kBACA,iBACA,cAGD,MACC,eACA,kBACA,mBAEA,UACC,eAIF,GACC,eAGD,EACC,qBACA,mBACA,MAjCW,QAmCX,QACC,MAtCU,QA0CZ,IACC,aACA,8BACA,mBACA,kBAEA,OACC,aACA,SACA,SACA,UAEA,UACC,gBACA,SACA,UAEA,YACC,MA5DQ,QA8DR,kBACC,MA7DO,QAmEX,aACC,qBACA,iBACA,cAzEa,IA0Eb,yBACA,MA1EU,QA2EV,mBACA,MA1EU,QA4EV,mBACC,WA7ES,QA8ET,WAKH,QACC,kBAGD,OACC,kBACA,eACA,mBACA,uBACA,aA7FW,QAiGX,eACC,aACA,SACA,2BACA,mBAEA,kBACC,SAGD,mBACC,gBACA,iBACA,eACA,cAjHY","file":"main.css"}

1
static/web/main.min.js vendored Normal file
View File

@@ -0,0 +1 @@
(()=>{console.log("Hello from Shifu!");})();

View File

@@ -1,93 +0,0 @@
{{template "head.html"}}
<div class="title">
<div>
<h1>marvin blum</h1>
<h2>welcome to my website!</h2>
</div>
<img src="../static/avatar_100.jpg" alt="Marvin Blum" />
</div>
{{template "menu.html"}}
<section>
<h2>Who Am I?</h2>
<p>
I'm a full stack software engineer from Germany, open source and Linux enthusiast and co-founder of <a href="https://emvi.com/" target="_blank">Emvi</a>.
In love with Go, but fluent in a lot of programming languages.
</p>
</section>
<section>
<h2>Latest Blog Posts</h2>
{{range $article := .Articles}}
<p>
<small>{{format $article.Published "2. January 2006"}}</small>
<br />
<a href="/blog/{{slug $article.LatestArticleContent.Title}}-{{$article.Id}}">{{$article.LatestArticleContent.Title}}</a>
</p>
{{else}}
<p>There are no blog posts yet...</p>
{{end}}
<p>
<a href="/blog">View all</a>
</p>
</section>
<section>
<h2>Projects</h2>
<ul>
<li>
<a href="https://emvi.com/" target="_blank">Emvi</a> a note taking, collaboration and knowledge management tool for personal use and teams of any size
</li>
<li>
<a href="https://github.com/pirsch-analytics/pirsch" target="_blank">pirsch</a> a server side, no-cookie and privacy focused tracking library for Golang
</li>
<li>
<a href="https://github.com/emvi/logbuch" target="_blank">logbuch</a> a simple Golang logging library
</li>
<li>
<a href="https://github.com/emvi/null" target="_blank">null</a> a Golang library for nullable types supporting databases and json
</li>
<li>
<a href="https://github.com/emvi/hide" target="_blank">hide</a> a Golang library to obscure IDs on the API layer
</li>
<li>
<a href="https://github.com/assetto-corsa-web/accweb" target="_blank">accweb</a> a web interface to manage game servers
</li>
</ul>
</section>
<section>
<h2>Skills</h2>
<ul>
<li>Go (Golang)</li>
<li>JavaScript (Vue, Node)</li>
<li>HTML, CSS, Sass and all the web fuzz</li>
<li>Java</li>
<li>PHP</li>
<li>Linux</li>
<li>Docker</li>
<li>Kubernetes</li>
<li>... and more</li>
</ul>
</section>
<section>
<h2>Work</h2>
<ul>
<li>
<a href="https://pirsch.io/" target="_blank">Pirsch</a> co-founder of a privacy-friendly, open-source, web analytics SaaS and library
</li>
<li>
<a href="https://emvi.com/" target="_blank">Emvi</a> co-founder of a note taking, collaboration and knowledge management SaaS startup
</li>
<li>
<a href="https://skalar.marketing/" target="_blank">skalar marketing</a> as a full stack developer and web designer
</li>
<li>
<a href="https://www.bertelsmann.com/divisions/arvato/#st-1" target="_blank">arvato</a> as a Java developer
</li>
<li>
some freelancing from time to time
</li>
</ul>
</section>
{{template "end.html"}}

View File

@@ -1,10 +0,0 @@
{{template "head.html"}}
{{template "menu.html"}}
<section>
<h1>{{.Title}}</h1>
<small>Published on {{format .Published "2. January 2006"}}</small>
{{.Content}}
</section>
{{template "end.html"}}

View File

@@ -1,25 +0,0 @@
{{template "head.html"}}
{{template "menu.html"}}
<section>
<h1>Blog</h1>
</section>
{{range $year, $articles := .Articles}}
<section>
<h2>{{$year}}</h2>
{{range $article := $articles}}
<p>
<small>{{format $article.Published "2. January 2006"}}</small>
<br />
<a href="/blog/{{slug $article.LatestArticleContent.Title}}-{{$article.Id}}">{{$article.LatestArticleContent.Title}}</a>
</p>
{{end}}
</section>
{{else}}
<section>
<p>There are no blog posts yet...</p>
</section>
{{end}}
{{template "end.html"}}

View File

@@ -1,14 +0,0 @@
<hr />
<section>
<p>
Would you like to see more? Read my blog articles on <a href="https://emvi.com/blog" target="_blank">Emvi</a>, my project page on <a href="https://github.com/Kugelschieber" target="_blank">GitHub</a> or send me a <a href="mailto:marvin@marvinblum.de">mail</a>.
</p>
<p>
This page uses <a href="https://concrete.style/" target="_blank">concrete</a> for styling. Check it out!
</p>
<p>
This page does not use cookies. <a href="/legal">Legal</a>
</p>
</section>
</body>
</html>

View File

@@ -1,34 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<base href="/" />
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="copyright" content="Marvin Blum" />
<meta name="author" content="Marvin Blum" />
<meta name="title" content="Marvin Blum" />
<meta name="description" content="A full stack software engineer from Germany, open source and Linux enthusiast and co-founder of Emvi." />
<meta name="msapplication-TileColor" content="#000000" />
<meta name="theme-color" content="#000000" />
<meta name="twitter:card" content="profile" />
<meta name="twitter:site" content="@m5blum" />
<meta name="twitter:title" content="Marvin Blum" />
<meta name="twitter:description" content="A full stack software engineer from Germany, open source and Linux enthusiast and co-founder of Emvi." />
<meta name="twitter:image" content="https://marvinblum.de/avatar.png" />
<meta property="og:url" content="https://marvinblum.de/" />
<meta property="og:title" content="Marvin Blum" />
<meta property="og:description" content="A full stack software engineer from Germany, open source and Linux enthusiast and co-founder of Emvi." />
<meta property="og:image" content="https://marvinblum.de/avatar.png" />
<title>marvin blum</title>
<link rel="apple-touch-icon" sizes="180x180" href="../static/favicon/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="../static/favicon/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="../static/favicon/favicon-16x16.png" />
<link rel="manifest" href="../static/favicon/site.webmanifest" />
<link rel="mask-icon" href="../static/favicon/safari-pinned-tab.svg" color="#5bbad5" />
<meta name="msapplication-TileColor" content="#da532c" />
<meta name="theme-color" content="#ffffff" />
<link rel="stylesheet" type="text/css" href="../static/normalize.css" />
<link rel="stylesheet" type="text/css" href="../static/concrete.css" />
<link rel="stylesheet" type="text/css" href="../static/style.css" />
</head>
<body>

View File

@@ -1,23 +0,0 @@
{{template "head.html"}}
{{template "menu.html"}}
<section>
<h1>Legal</h1>
</section>
<section>
<h2>According to §5 TMG</h2>
<p>
Marvin Blum<br />
Gerhard-Hauptmannstraße 3<br />
33378 Rheda-Wiedenbrück, Germany<br />
<a href="mailto:marvin@marvinblum.de">marvin@marvinblum.de</a>
</p>
</section>
<section>
<h2>Cookie Policy</h2>
<p>
This page does not use cookies.
</p>
</section>
{{template "end.html"}}

View File

@@ -1,7 +0,0 @@
<div class="menu">
<a href="/" class="button filled">About</a>
<a href="/blog" class="button filled">Blog</a>
<a href="mailto:marvin@marvinblum.de" class="button">Contact me</a>
<a href="https://github.com/Kugelschieber" target="_blank" class="button">GitHub</a>
<a href="https://twitter.com/m5blum" target="_blank" class="button">Twitter</a>
</div>

View File

@@ -1,12 +0,0 @@
{{template "head.html"}}
{{template "menu.html"}}
<section>
<h2>Page not found</h2>
<p>
Nothing to see here...<br />
<a href="/">Return home</a>
</p>
</section>
{{template "end.html"}}

2
tpl/end.html Normal file
View File

@@ -0,0 +1,2 @@
</body>
</html>

24
tpl/head.html Normal file
View File

@@ -0,0 +1,24 @@
{{- $title := copy .Page .Content "title" -}}
<!DOCTYPE html>
<html lang="{{.Page.Language}}">
<head>
<base href="/" />
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="copyright" content="{{copy .Page .Content "copyright"}}" />
<meta name="author" content="{{copy .Page .Content "author"}}" />
<meta name="title" content="{{$title}}" />
<meta name="description" content="{{copy .Page .Content "meta_description"}}" />
<link rel="canonical" href="{{.Page.CanonicalLink}}" />
{{range $language, $path := .Page.Path}}
<link rel="alternate" hreflang="{{if eq $language "en"}}x-default{{else}}{{$language}}{{end}}" href="{{fmt "%s%s" hostname $path}}" />
{{end}}
<link rel="stylesheet" type="text/css" href="/static/web/main.css?v={{.CMS.LastUpdate}}" />
<script src="/static/web/main.min.js?v={{.CMS.LastUpdate}}"></script>
<title>{{$title}}</title>
</head>
<body>

46
tpl/intro.html Normal file
View File

@@ -0,0 +1,46 @@
<nav>
<div>
<ul>
<li>
<a href="/">Home</a>
</li>
<li>
<a href="/blog">Blog</a>
</li>
<li>
<a href="https://github.com/Kugelschieber" target="_blank">GitHub</a>
</li>
<li>
<a href="https://x.com/m5blum" target="_blank">Twitter/X</a>
</li>
<li>
<a href="https://social.anoxinon.de/@m5blum" target="_blank">Mastodon</a>
</li>
</ul>
</div>
<div>
<a href="mailto:marvin.blum@emvi.com" class="contact">Contact me</a>
</div>
</nav>
<section class="intro">
<article>
<h1>Hi, I'm Marvin, co-founder of <a href="https://pirsch.io" target="_blank">Pirsch Analytics</a> and <a href="https://emvi.com" target="_blank">Emvi</a>, software engineer, and open-source enthusiast.</h1>
<img src="/static/img/me.jpg" alt="Picture" />
</article>
</section>
<section>
<h2>TODO</h2>
<ul>
<li>About</li>
<li>Skills</li>
<li>Projects</li>
<li>Work Experience</li>
<li>Hire me!</li>
</ul>
</section>
<footer>
<p>TODO Footer</p>
</footer>

View File

@@ -1,119 +0,0 @@
package tpl
import (
"bytes"
"fmt"
"github.com/emvi/logbuch"
"github.com/gosimple/slug"
"html/template"
"net/http"
"os"
"sync"
"time"
)
const (
templateDir = "template/*"
)
type Cache struct {
tpl *template.Template
cache map[string][]byte
hotReload bool
m sync.RWMutex
}
func NewCache() *Cache {
cache := &Cache{
cache: make(map[string][]byte),
hotReload: os.Getenv("MB_HOT_RELOAD") == "true",
}
logbuch.Debug("Template cache hot reload", logbuch.Fields{"hot_reload": cache.hotReload})
cache.load()
return cache
}
func (cache *Cache) load() {
logbuch.Debug("Loading templates")
funcMap := template.FuncMap{
"slug": slug.Make,
"format": func(t time.Time, layout string) string { return t.Format(layout) },
"multiply": func(a, b float64) float64 { return a * b },
"divide": func(a, b float64) float64 { return a / b },
"round": func(f float64) string { return fmt.Sprintf("%.2f", f) },
"intRange": intRange,
"float64": func(i int) float64 { return float64(i) },
}
var err error
cache.tpl, err = template.New("").Funcs(funcMap).ParseGlob(templateDir)
if err != nil {
logbuch.Fatal("Error loading template", logbuch.Fields{"err": err})
}
logbuch.Debug("Templates loaded", logbuch.Fields{"hot_reload": cache.hotReload})
}
func (cache *Cache) Render(w http.ResponseWriter, name string, data interface{}) {
cache.m.RLock()
if cache.cache[name] == nil || cache.hotReload {
cache.m.RUnlock()
cache.m.Lock()
defer cache.m.Unlock()
logbuch.Debug("Rendering template", logbuch.Fields{"name": name})
if cache.hotReload {
logbuch.Debug("Reloading templates")
cache.load()
}
var buffer bytes.Buffer
if err := cache.tpl.ExecuteTemplate(&buffer, name, data); err != nil {
logbuch.Error("Error executing template", logbuch.Fields{"err": err, "name": name})
w.WriteHeader(http.StatusInternalServerError)
}
cache.cache[name] = buffer.Bytes()
} else {
cache.m.RUnlock()
}
if _, err := w.Write(cache.cache[name]); err != nil {
logbuch.Error("Error sending response to client", logbuch.Fields{"err": err, "template": name})
w.WriteHeader(http.StatusInternalServerError)
}
}
func (cache *Cache) RenderWithoutCache(w http.ResponseWriter, name string, data interface{}) {
if cache.hotReload {
logbuch.Debug("Reloading templates")
cache.load()
}
if err := cache.tpl.ExecuteTemplate(w, name, data); err != nil {
logbuch.Error("Error executing template", logbuch.Fields{"err": err, "name": name})
w.WriteHeader(http.StatusInternalServerError)
}
}
func (cache *Cache) Clear() {
cache.m.Lock()
defer cache.m.Unlock()
cache.cache = make(map[string][]byte)
}
func intRange(start, end int) []int {
if end-start < 0 {
return []int{}
}
r := make([]int, end-start)
for i := 0; i < end-start; i++ {
r[i] = start + i
}
return r
}