Compare commits

..

5 Commits

Author SHA1 Message Date
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
84 changed files with 535 additions and 1961 deletions

3
.gitignore vendored
View File

@@ -1,3 +0,0 @@
.idea/
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,3 @@
# marvinblum.de
My website.
My website build using [Oogway](https://github.com/emvi/oogway).

View File

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig><msapplication><tile><square70x70logo src="/ms-icon-70x70.png"/><square150x150logo src="/ms-icon-150x150.png"/><square310x310logo src="/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
assets/favicon/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,41 @@
{
"name": "App",
"icons": [
{
"src": "\/android-icon-36x36.png",
"sizes": "36x36",
"type": "image\/png",
"density": "0.75"
},
{
"src": "\/android-icon-48x48.png",
"sizes": "48x48",
"type": "image\/png",
"density": "1.0"
},
{
"src": "\/android-icon-72x72.png",
"sizes": "72x72",
"type": "image\/png",
"density": "1.5"
},
{
"src": "\/android-icon-96x96.png",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
},
{
"src": "\/android-icon-144x144.png",
"sizes": "144x144",
"type": "image\/png",
"density": "3.0"
},
{
"src": "\/android-icon-192x192.png",
"sizes": "192x192",
"type": "image\/png",
"density": "4.0"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
assets/font/DMSans-Bold.ttf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

3
assets/icons/arrow.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="7" height="12" viewBox="0 0 7 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 11L1 6L6 1" stroke="#808080" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 190 B

4
assets/icons/cv.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" rx="8" fill="#212121"/>
<path d="M11.096 22.192C9.99733 22.192 9.048 21.952 8.248 21.472C7.45867 20.9813 6.84533 20.304 6.408 19.44C5.97067 18.5653 5.752 17.552 5.752 16.4C5.752 15.2587 5.97067 14.256 6.408 13.392C6.84533 12.5173 7.45867 11.8347 8.248 11.344C9.048 10.8533 9.99733 10.608 11.096 10.608C12.376 10.608 13.416 10.9173 14.216 11.536C15.0267 12.144 15.544 12.9973 15.768 14.096H14.28C14.1093 13.4027 13.7573 12.848 13.224 12.432C12.7013 12.0053 11.992 11.792 11.096 11.792C10.296 11.792 9.59733 11.9787 9 12.352C8.40267 12.7147 7.93867 13.2427 7.608 13.936C7.288 14.6187 7.128 15.44 7.128 16.4C7.128 17.36 7.288 18.1867 7.608 18.88C7.93867 19.5627 8.40267 20.0907 9 20.464C9.59733 20.8267 10.296 21.008 11.096 21.008C11.992 21.008 12.7013 20.8053 13.224 20.4C13.7573 19.984 14.1093 19.4347 14.28 18.752H15.768C15.544 19.8293 15.0267 20.672 14.216 21.28C13.416 21.888 12.376 22.192 11.096 22.192ZM21.0756 22L16.8676 10.8H18.3236L21.8596 20.576L25.4276 10.8H26.8516L22.6436 22H21.0756Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

3
assets/icons/github.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.0001 0C7.16466 0 0 7.33526 0 16.3841C0 23.6231 4.58452 29.7645 10.9419 31.931C11.7415 32.0827 12.0351 31.5756 12.0351 31.1428C12.0351 30.7521 12.0202 29.4615 12.0133 28.0924C7.56209 29.0836 6.62283 26.1593 6.62283 26.1593C5.89499 24.2655 4.8463 23.762 4.8463 23.762C3.39461 22.7451 4.95573 22.766 4.95573 22.766C6.56242 22.8816 7.40842 24.4544 7.40842 24.4544C8.83547 26.9592 11.1514 26.235 12.0645 25.8164C12.2081 24.7574 12.6227 24.0347 13.0803 23.6255C9.52647 23.2112 5.7906 21.8064 5.7906 15.5284C5.7906 13.7396 6.41563 12.2781 7.43916 11.1307C7.27303 10.718 6.72537 9.05159 7.59415 6.7948C7.59415 6.7948 8.93774 6.35445 11.9953 8.47423C13.2716 8.11122 14.6404 7.92916 16.0001 7.92292C17.3599 7.92916 18.7297 8.11122 20.0084 8.47423C23.0623 6.35445 24.404 6.7948 24.404 6.7948C25.2749 9.05159 24.727 10.718 24.5608 11.1307C25.5868 12.2781 26.2075 13.7396 26.2075 15.5284C26.2075 21.8213 22.4645 23.2069 18.9017 23.6125C19.4756 24.1209 19.9869 25.118 19.9869 26.6466C19.9869 28.8388 19.9684 30.6032 19.9684 31.1428C19.9684 31.5788 20.2564 32.0897 21.0674 31.9288C27.4213 29.7599 32 23.6206 32 16.3841C32 7.33526 24.8364 0 16.0001 0ZM5.99257 23.3395C5.95733 23.4209 5.83227 23.4454 5.71834 23.3895C5.60229 23.336 5.53711 23.2251 5.57474 23.1434C5.60918 23.0596 5.7345 23.0362 5.85029 23.0924C5.9666 23.1458 6.03284 23.2579 5.99257 23.3395ZM6.7796 24.0587C6.70329 24.1311 6.55412 24.0975 6.45291 23.983C6.34825 23.8687 6.32864 23.716 6.40601 23.6425C6.4847 23.57 6.62937 23.6039 6.73429 23.7182C6.83895 23.8337 6.85935 23.9854 6.7796 24.0587ZM7.31953 24.9787C7.2215 25.0485 7.0612 24.9831 6.96211 24.8374C6.86407 24.6917 6.86407 24.517 6.96422 24.447C7.06358 24.377 7.2215 24.4399 7.32191 24.5845C7.41968 24.7327 7.41968 24.9074 7.31953 24.9787ZM8.23267 26.0443C8.14497 26.1433 7.95818 26.1168 7.82146 25.9816C7.68156 25.8495 7.64261 25.662 7.73058 25.563C7.81934 25.4637 8.00719 25.4916 8.14497 25.6257C8.28381 25.7575 8.3262 25.9464 8.23267 26.0443ZM9.41281 26.404C9.37413 26.5324 9.19423 26.5907 9.013 26.5362C8.83203 26.48 8.7136 26.3297 8.75016 26.2C8.78778 26.0709 8.96848 26.0101 9.15104 26.0684C9.33174 26.1243 9.45044 26.2735 9.41281 26.404ZM10.7559 26.5566C10.7604 26.6917 10.6067 26.8038 10.4165 26.8062C10.2252 26.8106 10.0704 26.7012 10.0683 26.5683C10.0683 26.4318 10.2185 26.3209 10.4098 26.3176C10.6001 26.3138 10.7559 26.4223 10.7559 26.5566ZM12.0753 26.5048C12.0981 26.6367 11.9658 26.7721 11.7769 26.8082C11.5912 26.8429 11.4192 26.7615 11.3957 26.6307C11.3726 26.4956 11.5072 26.3602 11.6927 26.3252C11.8819 26.2916 12.0512 26.3708 12.0753 26.5048Z" fill="#212121"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,8 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" fill="#212121"/>
<path d="M32 0H0V32H32V0Z" fill="#212121"/>
<path d="M10.4 9.06665H7.2V22.9333H10.4V9.06665Z" fill="white"/>
<path d="M16.8 9.06665H13.6V22.9333H16.8V9.06665Z" fill="white"/>
<path d="M22.1333 14.4H16.2667V17.6H22.1333V14.4Z" fill="white"/>
<path d="M24.8 9.06665H21.6V22.9333H24.8V9.06665Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 455 B

3
assets/icons/twitter.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.0669 29C22.1394 29 28.7444 18.9957 28.7444 10.3226C28.7444 10.0413 28.7381 9.75382 28.7256 9.47256C30.0105 8.54336 31.1193 7.39242 32 6.0738C30.8033 6.6062 29.5329 6.95389 28.2319 7.10502C29.6017 6.28394 30.6274 4.99402 31.1187 3.47442C29.8301 4.23814 28.4208 4.77688 26.9513 5.06754C25.9611 4.01548 24.652 3.31888 23.2263 3.08546C21.8005 2.85204 20.3376 3.0948 19.0637 3.77619C17.7897 4.45759 16.7757 5.53967 16.1785 6.85515C15.5812 8.17062 15.4339 9.64622 15.7594 11.0538C13.15 10.9228 10.5973 10.245 8.26667 9.06422C5.93603 7.88336 3.87959 6.22598 2.23063 4.19942C1.39253 5.64439 1.13607 7.35429 1.51337 8.98149C1.89067 10.6088 2.87342 12.0314 4.26187 12.96C3.21951 12.927 2.19997 12.6463 1.2875 12.1413V12.2226C1.28657 13.739 1.8108 15.2088 2.77108 16.3824C3.73136 17.556 5.06843 18.3608 6.555 18.66C5.58941 18.9242 4.57597 18.9628 3.59313 18.7726C4.01261 20.0766 4.82877 21.2172 5.92769 22.0352C7.0266 22.8531 8.35347 23.3076 9.72313 23.335C7.39787 25.1616 4.52557 26.1522 1.56875 26.1476C1.04438 26.1468 0.520532 26.1146 0 26.0513C3.00381 27.9784 6.49804 29.0019 10.0669 29Z" fill="#212121"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
assets/marvin.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

230
assets/style.css Normal file
View File

@@ -0,0 +1,230 @@
:root {
--dark-gray: #212121;
--light-gray: #808080;
--lighter-gray: rgb(238, 238, 238);
}
@font-face {
font-family: "DM Sans";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("/assets/font/DMSans-Regular.ttf");
}
@font-face {
font-family: "DM Sans";
font-style: italic;
font-weight: 400;
font-display: swap;
src: url("/assets/font/DMSans-Italic.ttf");
}
@font-face {
font-family: "DM Sans";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url("/assets/font/DMSans-Bold.ttf");
}
@font-face {
font-family: "DM Sans";
font-style: italic;
font-weight: bold;
font-display: 700;
src: url("/assets/font/DMSans-BoldItalic.ttf");
}
html, body {
margin: 0;
padding: 0;
font-family: "DM Sans", sans-serif;
color: var(--dark-gray);
}
h1 {
font-size: 60px;
line-height: 78px;
font-weight: bold;
margin: 0 0 32px 0;
padding: 0;
}
h2 {
font-weight: bold;
font-size: 32px;
line-height: 42px;
margin: 0 0 16px 0;
padding: 0;
}
h2.name {
font-size: 18px;
line-height: 28px;
font-weight: normal;
margin: 0 0 16px 0;
padding: 0;
}
p {
font-size: 18px;
line-height: 28px;
margin: 0 0 28px 0;
padding: 0;
}
ul {
list-style-type: circle;
margin: 0 0 28px 0;
padding: 0 0 0 32px;
}
li {
font-size: 18px;
line-height: 28px;
}
pre, code {
font-family: monospace;
font-size: 14px;
line-height: 18px;
background: var(--lighter-gray);
padding: 2px;
border-radius: 2px;
}
pre {
margin: 0 0 28px 0;
padding: 24px;
border-radius: 8px;
overflow-x: auto;
}
button, .button {
width: 192px;
height: 54px;
color: #fff;
font-weight: bold;
font-size: 22px;
line-height: 29px;
display: flex;
justify-content: center;
align-items: center;
background: var(--dark-gray);
border-width: 0;
border-radius: 8px;
margin: 64px 0;
padding: 0;
cursor: pointer;
transition: opacity 0.1s linear;
}
button:hover, .button:hover {
color: #fff;
opacity: 0.9;
transition: opacity 0.1s linear;
}
a {
text-decoration: none;
color: var(--dark-gray);
transition: color 0.1s linear;
}
a:hover {
color: var(--light-gray);
transition: color 0.1s linear;
}
small {
font-size: 14px;
line-height: 18px;
color: var(--light-gray);
}
article a {
text-decoration: underline;
}
.content {
max-width: 640px;
margin: 128px;
}
.footer {
max-width: 540px;
border-width: 1px 0 0 0;
border-style: solid;
border-color: var(--light-gray);
margin: 64px 0 0 0;
padding: 32px 0 0 0;
}
.footer p {
margin: 0 0 18px 0;
}
.footer-icons {
display: inline-grid;
gap: 16px;
grid-auto-flow: column;
margin: 0 0 32px 0;
}
.footer-icons img {
transition: opacity 0.1s linear;
}
.footer-icons img:hover {
opacity: 0.9;
transition: opacity 0.1s linear;
}
.back-to-homepage {
display: inline-flex;
gap: 8px;
font-size: 14px;
line-height: 18px;
color: var(--light-gray);
margin: 0 0 16px 0;
}
.back-to-homepage:hover {
color: var(--dark-gray);
}
.back-to-homepage img {
width: 8px;
height: 16px;
}
.cv {
display: flex;
gap: 32px;
margin: 0 0 16px 0;
}
.cv div:first-child {
width: 128px;
color: var(--light-gray);
}
.fullpage-img {
position: fixed;
right: 0;
left: 896px;
top: 0;
bottom: 0;
overflow: hidden;
display: flex;
align-content: center;
justify-content: center;
}
.date {
font-size: 14px;
line-height: 18px;
color: var(--light-gray);
margin: 0 0 24px 0;
}

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()
}
}

3
config.toml Normal file
View File

@@ -0,0 +1,3 @@
[server]
host = "localhost"
port = 8080

View File

@@ -0,0 +1,89 @@
{{partial "head" .}}
<div class="content">
{{partial "back-to-homepage" .}}
<h1 style="margin-bottom: 16px;">How I Built My Website Using Emvi as a Headless CMS</h1>
<div class="date">14. June 2020</div>
<article>
<p>Welcome to my blog! My name is Marvin, I'm a software engineer and entrepreneur. I write about programming, servers, my work and everything I'm interested in. In my first blog post, I would like to show you how I build my website and the tools I used. You already guessed that from the title I suppose.</p>
<p>You can find the full source code for my website on GitHub. It's MIT licensed, so you can build your own on top of it or just reuse parts of the code.</p>
<h2>Goals</h2>
<p>So first of all, here are the goals I set when I started:</p>
<ul>
<li>the page must be self hosted, I do like to have full control</li>
<li>it must be fast and have a small footprint</li>
<li>easy to deploy on cheap hardware</li>
<li>I don't want to put too much thought and time into styling</li>
<li>enable me to write articles without having to change the page itself, but don't require me to install (and update!) a full fledged CMS at the same time</li>
</ul>
<p>The last point is probably the most important one to me. My page won't change very frequently and I don't want to maintain a CMS. I also don't want to write a template for any CMS out there, as that quickly gets out of hand and is not worth the effort. Static HTML won't do it neither, as the blog articles need to be updated as soon as I release a new one or change an existing one.</p>
<p>Lets go through the bullet points and the choices I made. The most interesting part is probably how I build the blog.</p>
<h2>Server and Deployment</h2>
<p>For hosting, I chose Hetzner as a cloud provider. The Hetzner cloud offers virtual machines, block storage and networking (subnets, floating IPs, ...). There is an API too, which can be used to automate things.</p>
<p>My website is hosted on the smallest VM (CX11-CEPH) for 2,96 €/month, which is insanely cheap. It provides a single vCPU, 2GB RAM and 20GB storage. Which is sufficient for my simple page. I chose a CEPH machine, as this will store all data on block storage rather than on the machine itself, which decouples it from the hardware. In case of a hardware fault, Hetzner will boot up my server on a different machine and I won't have to do anything. I'm not sure if it assigns a different IP to the server in that case. For the OS I chose Ubuntu as I use that on my computer and I'm familiar with it.</p>
<p>The software running my page is a custom server I build using Go (golang), as it is an excellent programming language and offers high performance. I will go into more detail about the code in a second.</p>
<p>I use Docker and Compose to deploy my page. Both are well established tools to package and deploy software. These are the only tools I installed on the VM, so I just need to update the systems packages through apt from time to time. Within the docker-compose.yml I added Traefik as a reverse proxy to schedule a SSL certificate from Letsencrypt.</p>
<p>Deploying my page is now as simple as building and pushing the Docker image, pulling it on the server and restarting the container. Of course you could automate that whole process so that the page updates itself, but again: I won't change the content frequently. So that's good enough.</p>
<h2>Structure and Static Content</h2>
<p>Lets taking a look at the directory structure:</p>
<ul>
<li>blog - code to load and cache blog articles</li>
<li>static - static files (my picture, stylesheets, ...) and used to cache blog article attachments (more on that later)</li>
<li>template - contains the HTML files to build the page</li>
<li>tpl - code to load and build the page from the template files</li>
</ul>
<p>The root directory contains the <code>main.go</code> to wire everything up and set up the router, as well as the <code>Dockerfile</code>, <code>docker-compose.yml</code> and the Go dependencies (<code>go.mod</code>). Everything within the <code>static</code> directory is served as static content on the <code>/static/...</code> route. Each page has it's own handler function which assembles the HTML using the template files.</p>
<p>Another point worth mentioning is gzip compression. I added the <code>gziphandler.GzipHandler</code> on the static route to compress files. The middleware is build by the New York Times and easy to integrate. You can check it out <a href="https://github.com/nytimes/gziphandler" target="_blank" rel="noreferrer">here</a>.</p>
<h2>Styling</h2>
<p>As I do like to keep things simple, I chose a micro CSS framework so that I don't have to bother with styling too much. Namely <a href="https://concrete.style/" target="_blank" rel="noreferrer">concrete</a>, which I adjusted a bit, to narrow the layout and add a header with my picture. Apart from that I'm quite pleased with the look of it. As a bonus, it also switches to dark mode automatically if you set that in your (OS) preferences.</p>
<h2>Templating</h2>
<p>To prevent writing the same HTML over and over again I made use of Go's template system. It's simple but powerful enough for most websites and you can extend it using function maps. Here is an example for the blog article page (the one you're looking at right now):</p>
<pre><code language="text/html">{{`{{template "head.html"}}`}}
{{`{{template "menu.html"}}`}}
&lt;section&gt;
&lt;h1&gt;{{"{{.Title}}"}}&lt;/h1&gt;
&lt;small&gt;Published on {{`{{format .Published "2. January 2006"}}`}}&lt;/small&gt;
{{"{{.Content}}"}}
&lt;/section&gt;
{{`{{template "end.html"}}`}}</code></pre>
<p><code>head</code>, <code>menu</code> and <code>end</code> are reused on all pages.</p>
<p>I've added two functions to format dates and build the blog article slug from the title:</p>
<pre><code language="text/x-go">var funcMap = template.FuncMap{
"slug": slug.Make,
"format": func(t time.Time, layout string) string {
return t.Format(layout)
},
}</code></pre>
<h2>Blog</h2>
<p><a href="https://emvi.com/" target="_blank" rel="noreferrer">Emvi</a> offers an API which allows anyone to use it as a headless CMS. The main advantage of it is, that I can use its editor to write my blog articles, upload images/files and don't need to worry about hosting my own CMS. Apart from that I'm using Emvi for note taking and documentation anyways, so I can stay on the same platform.</p>
<p>To read articles, I make use of the <a href="https://github.com/emvi/api-go" target="_blank" rel="noreferrer">Go client library</a>. It isn't complete yet, as Emvi is still in beta, but provides everything required to build a blog. On top of it I build my own type to cache articles and files and sort them into maps, which are rendered on my page later. You could just use the client to do all of that without caching, but to reduce latency and serve articles in case Emvi goes down for some reason, I thought that would be a good idea.</p>
<pre><code language="text/x-go">type Blog struct {
client *emvi.Client
articles map[string]emvi.Article // id -&gt; article
articlesYear map[int][]emvi.Article // year -&gt; articles
nextUpdate time.Time
}</code></pre>
<p>The <code>client</code> is initialized with the client ID and secret I generated within Emvi, as well as the name of my organization. These are configured using environment variables, so that I can put them into the <code>docker-compose.yml</code>. <code>nextUpdate</code> is used to refresh the cache after some time. Articles and files will only be updated in case they have changed since the last time they have been accessed. The article content itself is cached in memory, files are stored on disk.</p>
<p>Articles are put into two different maps. The first one is used to access any article by ID. The ID is read from the slug within the URL to render an article. The second map groups all articles by year, which is used to display them on the blogs overview page.</p>
<p>Note that you need to set an article to "external" within Emvi to allow it to be read through the API. To prevent reading articles which do not belong to my blog, I filtered the results by the tag "blog" and sort them in descending order:</p>
<pre><code language="text/x-go">results, _, err = blog.client.FindArticles("", &amp;emvi.ArticleFilter{
BaseSearch: emvi.BaseSearch{Offset: offset},
Tags: "blog",
SortPublished: emvi.SortDescending,
})</code></pre>
<p>The offset is provided to read articles in a loop, as you can only read a fixed amount of results in one call. Afterwards, the content and files are read and cached for all results. I also added some regex to replace the paths within the content of each article to read images and files from my page instead of accessing Emvi.</p>
<p>And that's pretty much it. If you now visit my website, it will extract the ID from the URL, look up the cache, update it if required and return the result to you.</p>
<h2>Conclusion</h2>
<p>Personal blogging is something I love about the internet and I now started my own blog. In terms of cost, running this page costs me 2,96 €/month for the server and 5$/month for Emvi (also I'm not paying for it as I'm the co-founder) plus something for the domain, which is insignificant. The solution I chose is fun and easy to implement, but certainly not suitable for non-programmers. I hope I can provide a plug and play solution in the future. It will most likely also use Emvi, as we are turning it into a platform for all sorts of different applications.</p>
<p>In case you would like to send me feedback or have a question, you can contact me by <a href="mailto:marvin@marvinblum.de" target="_blank" rel="noreferrer">mail</a> or on <a href="https://twitter.com/m5blum" target="_blank" rel="noreferrer">Twitter</a>.</p>
</article>
{{partial "footer" .}}
</div>
<div class="fullpage-img">
<img src="assets/blog/how-i-built-my-website-using-emvi-as-a-headless-cms/test.png" alt="Test" />
</div>
{{partial "end" .}}

41
content/cv/index.html Normal file
View File

@@ -0,0 +1,41 @@
{{partial "head" .}}
<div class="content">
{{partial "back-to-homepage" .}}
<h1>Curriculum Vitae</h1>
<h2>Work Experience</h2>
<div class="cv">
<div>
2020 — Now
</div>
<div>
Software Developer<br />
skalar GmbH
</div>
</div>
<div class="cv">
<div>
2018 — Now
</div>
<div>
Co-Founder<br />
Emvi Software GmbH
</div>
</div>
<div class="cv">
<div>
2014 — 2020
</div>
<div>
Software Developer<br />
arvato business support GmbH
</div>
</div>
{{partial "footer" .}}
</div>
<div class="fullpage-img">
<img src="assets/marvin.jpg" alt="Marvin" />
</div>
{{partial "end" .}}

13
content/index.html Normal file
View File

@@ -0,0 +1,13 @@
{{partial "head" .}}
<div class="content">
<h2 class="name">Marvin Blum <span style="color: #808080;">— Rheda-Wiedenbrück, Germany</span></h2>
<h1>Full-Stack<br />Software Developer</h1>
<p>Hi, my name is Marvin. I'm a full-stack software engineer from Germany, open-source and Linux enthusiast, and co-founder of Emvi, where we build and maintain software projects for clients, and work on existing software.</p>
<p>Im building Pirsch Analytics — a privacy-friendly, cookie-less web analytics solution made for personal websites, freelancers, and agencies.</p>
<a class="button" href="mailto:marvin@marvinblum.de">Get in touch</a>
{{partial "footer" .}}
</div>
{{partial "end" .}}

19
content/legal/index.html Normal file
View File

@@ -0,0 +1,19 @@
{{partial "head" .}}
<div class="content">
{{partial "back-to-homepage" .}}
<h1>Legal</h1>
<h2>According to §5 TMG</h2>
<p>
Marvin Blum<br />
Gerhard-Hauptmannstraße 3<br />
33378 Rheda-Wiedenbrück, Germany<br />
marvin@marvinblum.de
</p>
<h2>Cookie Policy</h2>
<p>This page does not use cookies.</p>
{{partial "footer" .}}
</div>
{{partial "end" .}}

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

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

@@ -0,0 +1,4 @@
<a href="/" class="back-to-homepage">
<img src="assets/icons/arrow.svg" alt="&lt;" />
Back to Homepage
</a>

2
partials/end.html Normal file
View File

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

24
partials/footer.html Normal file
View File

@@ -0,0 +1,24 @@
<div class="footer">
<div class="footer-icons">
<a href="https://github.com/Kugelschieber" target="_blank">
<img src="assets/icons/github.svg" alt="GitHub" />
</a>
<a href="https://twitter.com/m5blum" target="_blank">
<img src="assets/icons/twitter.svg" alt="Twitter" />
</a>
<a href="https://www.indiehackers.com/m5blum" target="_blank">
<img src="assets/icons/indiehackers.svg" alt="IndieHackers" />
</a>
<a href="/cv">
<img src="assets/icons/cv.svg" alt="GitHub" />
</a>
</div>
<p>
<small>© 2021 Marvin Blum. This page does not use cookies. Website Analytics by <a href="https://pirsch.io/" target="_blank">Pirsch</a>.</small>
</p>
<p>
<small>
<a href="/legal">Legal</a>
</small>
</p>
</div>

45
partials/head.html Normal file
View File

@@ -0,0 +1,45 @@
<!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 — Full-Stack Software Developer" />
<meta name="description" content="A full stack software engineer from Germany, open source and Linux enthusiast and co-founder of Emvi." />
<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/assets/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/assets/avatar.png" />
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="assets/favicon/ms-icon-144x144.png">
<meta name="theme-color" content="#ffffff">
<link rel="apple-touch-icon" sizes="57x57" href="assets/favicon/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="assets/favicon/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="assets/favicon/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="assets/favicon/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="assets/favicon/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="assets/favicon/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="assets/favicon/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="assets/favicon/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="assets/favicon/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="assets/favicon/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="assets/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="assets/favicon/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="assets/favicon/favicon-16x16.png">
<link rel="manifest" href="assets/favicon/manifest.json">
<link rel="stylesheet" type="text/css" href="assets/style.css" />
<title>Marvin Blum — Full-Stack Software Developer</title>
<script defer type="text/javascript" src="https://api.pirsch.io/pirsch.js"
id="pirschjs"
data-code="1vNLFfXQ6qdYSaa9x1CGNwLwFDbD19zT"></script>
</head>
<body>

View File

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

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"
}

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;
}
}

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"}}

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
}