Initial commit, migrating from Ghost

This commit is contained in:
Roger Gonzalez 2020-04-25 16:50:56 -03:00
commit 29d309db51
83 changed files with 3377 additions and 0 deletions

18
.gitignore vendored Normal file
View File

@ -0,0 +1,18 @@
.DS_Store
.idea
*.log
tmp/
# Created by https://www.gitignore.io/api/hugo
# Edit at https://www.gitignore.io/?templates=hugo
### Hugo ###
# Generated files by hugo
/public/
/resources/_gen/
# Executable may be added to repository
hugo.exe
hugo.darwin
hugo.linux
# End of https://www.gitignore.io/api/hugo

7
archetypes/default.md Normal file
View File

@ -0,0 +1,7 @@
---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
lastmod: {{ .Date }}
tags : [ "dev", "hugo" ]
draft: true
---

33
config.toml Normal file
View File

@ -0,0 +1,33 @@
baseURL = "https://blog.rogs.me"
languageCode = "en-us"
title = "rogs | Blog"
theme = "m10c"
staticDir = ["static"]
[permalinks]
posts = "/:year/:month/:title/"
[params]
author = "Roger Gonzalez"
description = "my blog, thoughts and other stuff"
avatar = "avatar.png"
[[params.social]]
name = "website"
url = "https://rogs.me"
[[params.social]]
name = "github"
url = "https://git.rogs.me/me"
[[params.social]]
name = "linkedin"
url = "https://linkedin.com/in/rogergonzalez21"
[[params.social]]
name = "twitter"
url = "https://twitter.com/rogergonzalez21"
[[params.social]]
name = "rss"
url = "https://blog.rogs.me/index.xml"
[params.style]
darkestColor = "#38761d"
darkColor = "#212121"
lightColor = "#fafafa"

View File

@ -0,0 +1,60 @@
---
title: "De-Google my life - Part 1 of ¯\_(ツ)_/¯: Why? How?"
url: "/2019/03/15/de-google-my-life-part-1-of-_-tu-_-why-how"
date: 2019-03-15T15:59:00-04:00
lastmod: 2020-04-25T12:35:53-03:00
tags : [ "degoogle", "devops" ]
---
Hi everyone! I'm here with my first project of the year. It is almost done, but I think it is time to start documenting everything.
One day I was hanging out with my girlfriend looking for trips to japan online and found myself bombarded by ads that were disturbingly specific. We realized at the moment that Google knows A LOT of us, and we were not happy about that. With my tech knowledge, I knew that there were a lot of alternatives to Google, but first I needed to answer a bigger question:
# Why?
I told my techie friends about the craziness I was trying to accomplish and they all answered in unison: Why?
So I came up with the following list:
* **Privacy**. The internet is a scary place if you don't know what you are doing. I don't like big corporations knowing everything about me just to sell ads or use my data for whatever they want. I have learned that if something is free it's because [**you** are the product](https://twitter.com/rogergonzalez21/status/1067816233125494784) **EXCEPT** in opensource (thanks to [/u/SnowKissedBerries](https://www.reddit.com/user/SnowKissedBerries) for that clarification.
* **Security**. I live in a very controlled country (Venezuela). Over here, almost every government agency is looking at you, so using selfhosted alternatives and a VPN is a peace of mind for me and my family.
* **To learn**. Learning all these skills are going to be good for my career as a Backend / DevOps engineer.
* **Because I can and it is fun**. Narrowing it all down, I'm doing this because I can. It might be overkill, dumb, unreliable **but** it is really fun. Learning new skills is always a good, fun experience, for me at least.
Perfect! I have all the "Whys" detailed, but how am I going to achieve all of this?
# How?
First of all, I went to the experts (shout out to [/r/selfhosted!](https://www.reddit.com/r/selfhosted)) and read all the interesting topics over there that I could use for my selfhostable endeavours. After 1 week of reading and researching, I came with the following setup:
2 servers, each one with the following stack:
* **Server 1: Mail server**
Mailcow for my SMTP / IMAP email server
* **Server 2: Everything-else server**
Nextcloud for my files, calendar, tasks and contacts
Some other apps (?) (More on that for the following posts)
I chose DigitalOcean for the hosting because it is cheap and I have a ton of experience with those servers (I have setup more than 100 servers on their platform).
For VPN I chose [PIA](https://www.privateinternetaccess.com/pages/buy-a-vpn/1218buyavpn?invite=U2FsdGVkX1_cGyzYzdmeUMjhrUAwTzDBCMY-PsW-pXA%2CSawh3XnBRwlSt_9084reCHGX1Kk). The criteria for this decision was that one of my friends borrowed me his account for ~2 weeks and it worked super quick. Sometimes I didn't realize I was connected to the VPN on because the internet was super fast.
# Some self-imposed challenges
I knew this wasn't going to be easy, so of course I added more challenges just because <s>I'm dumb</s>.
* **Only use open source software**
I wasn't going to install more proprietary software on my servers. I wanted free and open source alternatives for my setup.
* **Only use Docker**
I had "Learn docker" in my backlog for too long, so I used this opportunity to learn it the hard way.
* **Use a cheap but reliable backup solution**
One of the parts that scared me about having my own servers was the backups. If one of the servers goes down, almost all of my work goes with it, so I needed to have a reliable but cheap backup solution.
# Conclusion
This is only the first part, but I'm planning on this being a long and very cool project. I hope I didn't bore you to death with all my yapping, I promise my next post will be more entertaining with code, server configurations, and all of that good stuff.
[Click here for part 2](https://blog.rogs.me/2019/03/22/de-google-my-life-part-2-of-_-tu-_-servers-and-emails/)
[Click here for part 3](https://blog.rogs.me/2019/03/29/de-google-my-life-part-3-of-_-tu-_-nextcloud-collabora/)
[Click here for part 4](https://blog.rogs.me/2019/11/20/de-google-my-life-part-4-of-_-tu-_-dokuwiki-ghost/)
[Click here for part 5](https://blog.rogs.me/2019/11/27/de-google-my-life-part-5-of-_-tu-_-backups/)

View File

@ -0,0 +1,129 @@
---
title: "De-Google my life - Part 2 of ¯\_(ツ)_/¯: Servers and Emails"
url: "/2019/03/22/de-google-my-life-part-2-of-_-tu-_-servers-and-emails"
date: 2019-03-22T21:03:00-04:00
lastmod: 2020-04-25T12:35:53-03:00
tags : [ "degoogle", "devops" ]
---
Hello everyone! Welcome to the second post of this blog series that aims to de-google my life as much as possible. If you haven't read the first one, you should [definitely check it out](https://blog.rogs.me/2019/03/15/de-google-my-life-part-1-of-_-tu-_-why-how/). On this delivery we'll focus more on code and configurations so I promise you it won't be as boring :)
# Servers configuration
As I mentioned on the previous post, I'll be using two servers that are going to be configured almost the same, so I'm going to explain it only one time. In order to host my servers I'm using [DigitalOcean](https://m.do.co/c/cf0ff9cae16a) because I'm very used to their UI, their prices are excelent and they accept Paypal. If you haven't yet, you should check them out.
To start, I'm using their $5 server which at the time of this writing includes:
* Ubuntu 18.04 64 bits
* 1GB RAM
* 1 CPU
* 1000 GB of monthly transfers
## Installation
On my first SSH to the server I perform basic tasks such as updating and upgrading the server:
sudo apt update && sudo apt ugrade - y
Then I install some essentials like Ubuntu Common Properties (used to add new repositories using `add-apt-repository`) NGINX, HTOP, GIT and Emacs, the best text editor in this planet <small>vim sucks</small>
sudo apt install software-properties-common nginx htop git emacs
For SSL certificates I'm going to use Certbot because it is the most simple and usefull tool for it. This one requires some extra steps:
sudo add-apt-repository ppa:certbot/certbot -y
sudo apt update
sudo apt install python-certbot-nginx -y
By default DigitalOcean servers have no `swap`, so I'll add it by pasting some [DigitalOcean boilerplate](https://www.digitalocean.com/community/tutorials/how-to-add-swap-space-on-ubuntu-18-04) on to the terminal:
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
sudo cp /etc/fstab /etc/fstab.bak
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
sudo sysctl vm.swappiness=10
sudo sysctl vm.vfs_cache_pressure=50
sudo echo "vm.swappiness=10" >> /etc/sysctl.conf
sudo echo "vm.vfs_cache_pressure=50" >> /etc/sysctl.conf
This adds 2GB of `swap`
Then I set up my firewall with UFW:
sudo ufw allow 22 #SSH
sudo ufw allow 80 #HTTP
sudo ufw allow 443 #HTTPS
sudo ufw allow 25 #IMAP
sudo ufw allow 143 #IMAP
sudo ufw allow 993 #IMAPS
sudo ufw allow 110 #POP3
sudo ufw allow 995 #POP3S
sudo ufw allow 587 #SMTP
sudo ufw allow 465 #SMTPS
sudo ufw allow 4190 #Manage Sieve
sudo ufw enable
Finally, I install `docker` and `docker-compose`, which are going to be the main software running on both servers.
# Docker
curl -sSL https://get.docker.com/ | CHANNEL=stable sh
systemctl enable docker.service
systemctl start docker.service
# Docker compose
curl -L https://github.com/docker/compose/releases/download/$(curl -Ls https://www.servercow.de/docker-compose/latest.php)/docker-compose-$(uname -s)-$(uname -m) > /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
Now that everything is done, we can continue configuring the first server!
# Server #1: Mailcow
For my email I chose Mailcow. Why?
* It checks all of my "challenges list" items from last week's post ([open source and dockerized](https://github.com/mailcow/mailcow-dockerized)).
* The documentation is fantastic, explaining each detail one by one.
* It has a huge community behind it.
## Installation & Setup
Installation was simple, first I followed the instructions on their [official documentation](https://mailcow.github.io/mailcow-dockerized-docs/i_u_m_install/)
cd /opt
git clone https://github.com/mailcow/mailcow-dockerized
cd mailcow-dockerized
./generate_config.sh
# The process will ask you for your FQDN to automatically configure NGINX.
# Mine is mail.rogs.me, but yours might be whatever you want
I pointed my subdomain (an A record in Cloudflare) and I finally opened my browser and visited [https://mail.rogs.me](https://mail.rogs.me) and there it was, beautiful as I was expecting.
![Captura-de-pantalla-de-2019-03-20-17-20-49](/Captura-de-pantalla-de-2019-03-20-17-20-49.png)
<small>What a beautiful cow</small>
After that I just followed the documentation to [configure their Let's Encrypt docker image](https://mailcow.github.io/mailcow-dockerized-docs/firststeps-ssl/), [added more records on my DNS](https://mailcow.github.io/mailcow-dockerized-docs/prerequisite-dns/) and tested a lot with [https://www.mail-tester.com/](https://www.mail-tester.com/) until I got a good score
![Captura-de-pantalla-de-2019-03-20-17-25-14](/Captura-de-pantalla-de-2019-03-20-17-25-14.png)
<small>My actual score. Everything is perfect in self-hosted-mail-land</small>
I know that sometimes that score doesn't mean much, but at least is nice to know my email is completely configured.
## Backups
Since I keep all my emails local, I didn't want a huge backup solution for this server, so I went with the DigitalOcean backup, which costs $1 per month. Cheap, reliable and it just works.
## Edit Nov 23-26 2019
As of now, I'm not using PIA anymore because [they where bought by Kape Technologies, which is known for sending malware through their software and for being scummy in general.](https://www.reddit.com/r/homelab/comments/e05ce4/psa_piaprivateinternetaccess_has_been_bought_by/). I'm now using [Mullvad](https://mullvad.net/), [which really focuses on security](https://mullvad.net/es/help/no-logging-data-policy/). If you were using PIA, I really recommend you change providers.
# Conclusion
With all of this my first server was done, but it was also the easiest. This one was a pretty straightforward installation with nothing fancy going on: No backups, no NGINX configuration, nothing much. On the good side, I had my email working really quick and it was a very satisfying and rewarding experience. This is when the "selfhost everything" bug bit me and this project really started ramp up in speed. On the next post we will talk about the second server, which includes fun stuff as [Nextcloud](https://nextcloud.com/), [Collabora](https://www.collaboraoffice.com/), [Dokuwiki](https://www.dokuwiki.org/dokuwiki) and many more.
Stay tuned!
[Click here for part 3](https://blog.rogs.me/2019/03/29/de-google-my-life-part-3-of-_-tu-_-nextcloud-collabora/)
[Click here for part 4](https://blog.rogs.me/2019/11/20/de-google-my-life-part-4-of-_-tu-_-dokuwiki-ghost/)
[Click here for part 5](https://blog.rogs.me/2019/11/27/de-google-my-life-part-5-of-_-tu-_-backups/)

View File

@ -0,0 +1,279 @@
---
title: "De-Google my life - Part 3 of ¯\_(ツ)_/¯: Nextcloud & Collabora"
url: "/2019/03/29/de-google-my-life-part-3-of-_-tu-_-nextcloud-collabora"
date: 2019-03-28T19:07:00-04:00
lastmod: 2020-04-25T12:35:53-03:00
tags : [ "degoogle", "devops" ]
---
<div class="kg-card-markdown">
Hello everyone! Welcome to the third post of my blogseries "De-Google my life". If you haven't read the other ones you definitely should! ([Part 1](https://blog.rogs.me/2019/03/15/de-google-my-life-part-1-of-_-tu-_-why-how/), [Part 2](https://blog.rogs.me/2019/03/22/de-google-my-life-part-2-of-_-tu-_-servers-and-emails/)). Today we are moving forward with one of the most important apps I'm running on my servers: [Nextcloud](https://nextcloud.com/). A big part of my Google usage was Google Drive (and all it's derivate apps). With Nextcloud I was looking to replace:
* Docs
* Drive
* Photos
* Contacts
* Calendar
* Notes
* Tasks
* More (?)
I also wanted some new features, like connecting to a S3 bucket directly from my server and have a web interface to interact with it.
The first step is to set up the server. I'm not going to explain that again, but if you want to read more about that, I explain it a bit better on the [second post](https://blog.rogs.me/2019/03/22/de-google-my-life-part-2-of-_-tu-_-servers-and-emails/)
# Nextcloud
## Installation
For my Nextcloud installation I went straight to the [official docker documentation](https://github.com/nextcloud/docker) and extracted this docker compose:
version: '2'
volumes:
nextcloud:
db:
services:
db:
image: mariadb
command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW
restart: always
volumes:
- db:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=my_super_secure_root_password
- MYSQL_PASSWORD=my_super_secure_password
- MYSQL_DATABASE=nextcloud
- MYSQL_USER=nextcloud
app:
image: nextcloud
ports:
- 8080:80
links:
- db
volumes:
- nextcloud:/var/www/html
restart: always
**Some mistakes were made**
I forgot to mount the volumes and Docker automatically mounted them in /var/lib/docker/volumes/. This was a small problem I haven't solved yet because it hasn't bringed any serious issues. If someone knows if this is going to be problematic in the long run, please let me know. I didn't wanted to fix this just for the posts, I'm writing about my experience and of course it wasn't perfect.
I created the route `/opt/nextcloud` to keep my docker-compose file and finally ran:
docker-compose pull
docker-compose up -d
It was that simple! The app was running on port 8080! But that is not what I wanted. I wanted it running on port 80 and 443\. For that I used a reverse proxy with NGINX and Let's Encrypt
## NGINX configuration
Configuring NGINX is dead simple. Here is my configuration
`/etc/nginx/sites-available/nextcloud:`
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name myclouddomain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
server_name myclouddomain.com;
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
ssl on;
ssl_certificate /etc/letsencrypt/live/myclouddomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/myclouddomain.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forward-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_redirect off;
proxy_read_timeout 5m;
}
location = /.well-known/carddav {
return 301 $scheme://$host/remote.php/dav;
}
location = /.well-known/caldav {
return 301 $scheme://$host/remote.php/dav;
}
# Set the client_max_body_size to 1000M so NGINX doesn't cut uploads
client_max_body_size 1000M;
}
Then I created a soft link from the configuration file to the "sites enabled" folder:
ln -s /etc/nginx/sites-available/nextcloud /etc/nginx/sites-enabled
and that was it!
In this configuration you will see that I'm already referencing the SSL certificates even though they don't exist yet. We are going to create them on the next step.
## Let's Encrypt configuration
To generate the SSL certificates first you need to point your domain/subdomain to your server. Every DNS manager is different, so you will have to figure that out. The command I will use throught this blog series to create certificates is the following:
sudo -H certbot certonly --nginx-d mydomain.com
The first time you run Let's Encrypt, you have to configure some stuff. They will ask you for your email and some questions. Input that information and finish the process.
To enable automatic SSL certificates renovation, create a new cron job (`crontab -e`) with the following information:
0 3 * * * certbot renew -q
This will run every morning at 3AM and check if any of your domains needs to be renewed. If they do, it will renew it.
At the end, you should be able to visit [https://myclouddomain.com](https://myclouddomain.com) and be greeted with a nice NextCloud screen:
![Captura-de-pantalla-de-2019-03-28-10-51-04](/Captura-de-pantalla-de-2019-03-28-10-51-04.png)
<small>Beautiful yet frustrating blue screen</small>
## Nextcloud configuration
In this part I got super stuck. I had everything up and running, but I couldn't get my database to connect. It was SUPER FRUSTRATING. This is why I had failed:
Since in my docker-compose file I called the MariaDB docker `db`, the database host was not `localhost` but `db`.
Once that was fixed, Nextcloud was 100% ready to be used!
![Captura-de-pantalla-de-2019-03-28-16-19-13](/Captura-de-pantalla-de-2019-03-28-16-19-13.png)
After that I went straight to "Settings/Basic settings" and noticed that my background jobs were set to "Ajax". That's not good, because if I don't open the site, the tasks will never run. I changed it to "Cron" and created a new cron on my server with the following information:
*/15 * * * * /usr/bin/docker exec --user www-data nextcloud_app_1 php cron.php
This will run the Nextcloud cronjob in the docker machine every 15 mins.
Then, in "Settings/Overview" I noticed a bunch of errors on the "Security & setup warnings" part. Those were very easy to fix, but since all installations aren't the same I won't go deep into this. [DuckDuckGo](https://duckduckgo.com/) is your friend.
## Extra stuff
The Nextcloud apps store is filled with some interesting applications. The ones I have installed are:
* [Contacts](https://apps.nextcloud.com/apps/contacts)
* [Calendar](https://apps.nextcloud.com/apps/calendar)
* [Notes](https://apps.nextcloud.com/apps/notes)
* [Tasks](https://apps.nextcloud.com/apps/tasks)
* [Markdown editor](https://apps.nextcloud.com/apps/files_markdown)
* [PhoneTrack](https://apps.nextcloud.com/apps/phonetrack)
But you can add as many as you want! You can check them out [here](https://apps.nextcloud.com/)
# Collabora
Now that NextCloud was up and running, I needed my "Google Docs" part. Enter Collabora!
## Installation
If you don't know what it is, Collabora is like Google Docs / Sheets / Slides but free and open source. You can check more about the project [here](https://nextcloud.com/collaboraonline/)
This was a very easy installation. I ran it directly with docker:
docker run -t -d -p 127.0.0.1:9980:9980 -e 'domain=mynextclouddomain.com' --restart always --cap-add MKNOD collabora/code
Created a new NGINX reverse proxy
`/etc/nginx/sites-available/collabora`:
# Taken from https://icewind.nl/entry/collabora-online/
server {
listen 443 ssl;
server_name office.mydomain.com;
ssl_certificate /etc/letsencrypt/live/office.mydomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/office.mydomain.com/privkey.pem;
# static files
location ^~ /loleaflet {
proxy_pass https://localhost:9980;
proxy_set_header Host $http_host;
}
# WOPI discovery URL
location ^~ /hosting/discovery {
proxy_pass https://localhost:9980;
proxy_set_header Host $http_host;
}
# main websocket
location ~ ^/lool/(.*)/ws$ {
proxy_pass https://localhost:9980;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $http_host;
proxy_read_timeout 36000s;
}
# download, presentation and image upload
location ~ ^/lool {
proxy_pass https://localhost:9980;
proxy_set_header Host $http_host;
}
# Admin Console websocket
location ^~ /lool/adminws {
proxy_pass https://localhost:9980;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $http_host;
proxy_read_timeout 36000s;
}
}
Created the SSL certificate for the collabora installation:
sudo -H certbot certonly --nginx-d office.mydomain.com
And finally I created a soft link from the configuration file to the "sites enabled" folder:
ln -s /etc/nginx/sites-available/collabora /etc/nginx/sites-enabled
Pretty easy stuff.
## Nextcloud configuration
In Nextcloud I installed "Collabora" from the "Apps" menu. On "Settings/Collabora Online" I added my Collabora URL, applied it and voila!
![Captura-de-pantalla-de-2019-03-28-14-53-08](/Captura-de-pantalla-de-2019-03-28-14-53-08.png)
<small>Sweet Libre Office feel</small>
# S3 bucket
One of my biggest motivation for this project was a cheap, long term storgage solution for some files I don't interact with every day. I'm talking music, movies, videos, ISOS, etc. I used to have a bunch of HDD's but because of all the power outages in Venezuela, almost all my HDDs have died, and new ones are very expensive here, not to say all the issues we have with importing them from the US.
I wanted to look for something S3 like, but as cheap as possible.
In my investigations I found [Wasabi](https://wasabi.com/). Not only it was S3 like, but it was **dirt cheap**. $6 per month for 1TB of data. 1TB OF DATA FOR $6!! I could not believe it!
I created an account and installed the "external storage support" plugin in Nextcloud. After it was installed, I went to "Settings/External Storages" and filled up the information:
![Captura-de-pantalla-de-2019-03-28-15-32-50](/Captura-de-pantalla-de-2019-03-28-15-32-50.png)
![Captura-de-pantalla-de-2019-03-28-15-34-18](/Captura-de-pantalla-de-2019-03-28-15-34-18.png)
<small>My bucket name is "long-term-storage" and my local folder name is "Long term storage". You will need to generate API keys for the connection.</small>
I applied the changes and that was it! I could not believe how simple it was, so I uploaded a file just to test:
![Captura-de-pantalla-de-2019-03-28-15-38-38](/Captura-de-pantalla-de-2019-03-28-15-38-38.png)
![Captura-de-pantalla-de-2019-03-28-15-39-12](/Captura-de-pantalla-de-2019-03-28-15-39-12.png)
<small>[Classic _noice_ meme](https://knowyourmeme.com/memes/noice) uploaded in Nextcloud, ready to download in Wasabi. _toungue sound_ **Nice**</small>
# Conclusion
The project is looking good! In one sitting I have replaced almost every Google product and even added a humungus amount of storage (virtually infinite!) to the project. For the next delivery I'll add new and fun stuff I always wanted to host myself, like a Wiki, a [Blog](https://blog.rogs.me) (this very same blog!) and many more!
Stay tuned.
[Click here for part 4](https://blog.rogs.me/2019/11/20/de-google-my-life-part-4-of-_-tu-_-dokuwiki-ghost/)
[Click here for part 5](https://blog.rogs.me/2019/11/27/de-google-my-life-part-5-of-_-tu-_-backups/)
</div>

View File

@ -0,0 +1,134 @@
---
title: "De-Google my life - Part 4 of ¯\_(ツ)_/¯: Dokuwiki & Ghost"
url: "/2019/11/20/de-google-my-life-part-4-of-_-tu-_-dokuwiki-ghost"
date: 2019-11-20T19:29:00-03:00
lastmod: 2020-04-25T12:35:53-03:00
tags : [ "degoogle", "devops" ]
---
Hello everyone! Welcome to the fourth post of my blogseries "De-Google my life". If you haven't read the other ones you definitely should! ([Part 1](https://blog.rogs.me/2019/03/15/de-google-my-life-part-1-of-_-tu-_-why-how/), [Part 2](https://blog.rogs.me/2019/03/22/de-google-my-life-part-2-of-_-tu-_-servers-and-emails/), [Part 3](https://blog.rogs.me/2019/03/29/de-google-my-life-part-3-of-_-tu-_-nextcloud-collabora/)).
First of all, sorry for the long wait. I had a couple of IRL things to take care of (we will discuss those in further posts, I promise ( ͡° ͜ʖ ͡°)), but now I have plenty of time to work on more blog posts and other projects. Thanks for sticking around, and if you are new, welcome to this journey!
On this post, we get to the fun part: What am I going to do to improve my online presence? I began with the simplest answer: A blog (this very same blog you are reading right now lol)
# Ghost
[Ghost](https://ghost.org/) is an open source, headless blogging platform made in NodeJS. The community is quite large and most importantly, it fitted all my requirements (Open source and runs in a docker container).
For the installation, I kept it simple. I went to the [DockerHub page for Ghost](https://hub.docker.com/_/ghost/) and used their base `docker-compose` config for myself. This is what I came up with:
version: '3.1'
services:
ghost:
image: ghost:1-alpine
restart: always
ports:
- 7000:2368
environment:
database__client: mysql
database__connection__host: db
database__connection__user: root
database__connection__password: my_super_secure_mysql_password
database__connection__database: ghost
url: https://blog.rogs.me
db:
image: mysql:5.7
restart: always
environment:
MYSQL_ROOT_PASSWORD: my_super_secure_mysql_password
Simple enough. The base ghost image and a MySQL db image. Simple, readable, functional.
For the NGINX configuration I used a simple proxy:
server {
listen 80;
listen [::]:80;
server_name blog.rogs.me;
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
location / {
proxy_pass http://127.0.0.1:7000;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forward-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_redirect off;
proxy_read_timeout 5m;
}
client_max_body_size 10M;
}
What does this mean? This config is just "Hey NGINX! proxy port 7000 through port 80 please, thanks"
And that was it. So simple, there's nothing much to say. Just like the title of the series,`¯\_(ツ)_/¯`
![Captura-de-pantalla-de-2019-11-16-19-52-30](/Captura-de-pantalla-de-2019-11-16-19-52-30.png)
After that, it was just configuration and setup. I modified [this theme](https://github.com/kathyqian/crisp) to match a little more with my website colors and themes. I think it came out pretty nice :)
# Dokuwiki
I have always admired tech people that have their own wikis. It's like a place where you can find more about them in a fast and easy way: What they use, what their configurations are, tips, cheatsheets, scripts, anything! I don't consider myself someone worthy of a wiki, but I wanted one just for the funsies.
While doing research, I found [Dokuwiki](https://www.dokuwiki.org/dokuwiki), which is not only open source, but it uses no database! Everything is kept in files which compose your wiki. P R E T T Y N I C E.
On this one, DockerHub had no oficial Dokuwiki image, but I used a very good one from the user [mprasil](https://hub.docker.com/r/mprasil/dokuwiki). I used his recommended configuration (no `docker-compose` needed since it was a single docker image):
docker run -d -p 8000:80 --name my_wiki \
-v /data/docker/dokuwiki/data:/dokuwiki/data \
-v /data/docker/dokuwiki/conf:/dokuwiki/conf \
-v /data/docker/dokuwiki/lib/plugins:/dokuwiki/lib/plugins \
-v /data/docker/dokuwiki/lib/tpl:/dokuwiki/lib/tpl \
-v /data/docker/dokuwiki/logs:/var/log \
mprasil/dokuwiki
**Some mistakes were made, again**
I was following instructions blindly, I'm dumb. I mounted the Dokuwiki files on the /data/docker directory, which is not what I wanted. In the process of working on this project, I have learned one big thing:
_Always. check. installation. folders and/or mounting points_
Just like the last one, I didn't want to fix this just for the posts, I'm writing about my experience and of course it wasn't perfect.
Let's continue. Once the docker container was running, I configured NGINX with another simple proxy redirect:
server {
listen 80;
listen [::]:80;
server_name wiki.rogs.me;
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forward-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_redirect off;
proxy_read_timeout 5m;
}
client_max_body_size 10M;
}
Just as the other one: "Hey NGINX! Foward port 8000 to port 80 please :) Thanks!"
![Captura-de-pantalla-de-2019-11-16-20-15-35](/Captura-de-pantalla-de-2019-11-16-20-15-35.png)
<small>Simple dokuwiki screen, nothing too fancy</small>
Again, just like the other one, configuration and setup and voila! Everything was up and running.
# Conclusion
I was getting the hang of this "Docker" flow. There were mistakes, yes, but nothing too critical that would hurt me in the long run. Everything was running smoothly, and just with a few commands I had everything running and proxied. Just what I wanted.
Stay tuned for the next delivery, where I'm going to talk about GPG encrypted backups to an external Wasabi "S3 like" bucket. I promise this one won't take 8 months.
[Click here for part 5](https://blog.rogs.me/2019/11/27/de-google-my-life-part-5-of-_-tu-_-backups/)

View File

@ -0,0 +1,188 @@
---
title: "De-Google my life - Part 5 of ¯\_(ツ)_/¯: Backups"
url: "/2019/11/27/de-google-my-life-part-5-of-_-tu-_-backups"
date: 2019-11-27T19:30:00-04:00
lastmod: 2020-04-25T12:35:53-03:00
tags : [ "degoogle", "devops" ]
---
Hello everyone! Welcome to the fifth post of my blog series "De-Google my life". If you haven't read the other ones you definitely should! ([Part 1](https://blog.rogs.me/2019/03/15/de-google-my-life-part-1-of-_-tu-_-why-how/), [Part 2](https://blog.rogs.me/2019/03/22/de-google-my-life-part-2-of-_-tu-_-servers-and-emails/), [Part 3](https://blog.rogs.me/2019/03/29/de-google-my-life-part-3-of-_-tu-_-nextcloud-collabora/), [Part 4](https://blog.rogs.me/2019/11/20/de-google-my-life-part-4-of-_-tu-_-dokuwiki-ghost/)).
At this point, our server is up and running and everything is working 100% fine, but we can't always trust that. We need a way to securely backup everything in a place where we can restore quickly if needed.
# Backup location
My backups location was an easy choice. I already had a Wasabi subscription, so why not use it to save my backups as well?
I created a new bucket on Wasabi, just for my backups and that was it.
![Captura-de-pantalla-de-2019-11-24-18-13-55](/Captura-de-pantalla-de-2019-11-24-18-13-55.png)
<small>There is my bucket, waiting for my _sweet sweet_ backups</small>
# Security
Just uploading everything to Wasabi wasn't secure enough for me, so I'm encrypting my tar files with GPG.
## What is GPG?
From their website:
> GnuPG ([GNU Privacy Guard](https://gnupg.org/)) is a complete and free implementation of the OpenPGP standard as defined by RFC4880 (also known as PGP). GnuPG allows you to encrypt and sign your data and communications; it features a versatile key management system, along with access modules for all kinds of public key directories. GnuPG, also known as GPG, is a command-line tool with features for easy integration with other applications. A wealth of frontend applications and libraries are available. GnuPG also provides support for S/MIME and Secure Shell (ssh).
So, by using GPG I can encrypt my files before uploading to Wasabi, so if for any reason there is a leak, my files will still be protected by my GPG password.
# Script
## Nextcloud
#!/bin/sh
# Nextcloud
echo "======================================"
echo "Backing up Nextcloud"
cd /var/lib/docker/volumes/nextcloud_nextcloud/_data/data/roger
NEXTCLOUD_FILE_NAME=$(date +"%Y_%m_%d")_nextcloud_backup
echo $NEXTCLOUD_FILE_NAME
echo "Compressing"
tar czf /root/$NEXTCLOUD_FILE_NAME.tar.gz files/
echo "Encrypting"
gpg --passphrase-file the/location/of/my/passphrase --batch -c /root/$NEXTCLOUD_FILE_NAME.tar.gz
echo "Uploading"
aws s3 cp /root/$NEXTCLOUD_FILE_NAME.tar.gz.gpg s3://backups-cloud/Nextcloud/$NEXTCLOUD_FILE_NAME.tar.gz.gpg --endpoint-url=https://s3.wasabisys.com
echo "Deleting"
rm /root/$NEXTCLOUD_FILE_NAME.tar.gz /root/$NEXTCLOUD_FILE_NAME.tar.gz.gpg
### A breakdown
#!/bin/sh
This is to specify this is a shell script. The standard for this type of scripts.
# Nextcloud
echo "======================================"
echo "Backing up Nextcloud"
cd /var/lib/docker/volumes/nextcloud_nextcloud/_data/data/roger
NEXTCLOUD_FILE_NAME=$(date +"%Y_%m_%d")_nextcloud_backup
echo $NEXTCLOUD_FILE_NAME
Here, I `cd`ed to where my Nextcloud files are located. On [De-Google my life part 3](https://blog.rogs.me/2019/03/29/de-google-my-life-part-3-of-_-tu-_-nextcloud-collabora/) I talk about my mistake of not setting my volumes correctly, that's why I have to go to this location. I also create a new filename for my backup file using the current date information.
echo "Compressing"
tar czf /root/$NEXTCLOUD_FILE_NAME.tar.gz files/
echo "Encrypting"
gpg --passphrase-file the/location/of/my/passphrase --batch -c /root/$NEXTCLOUD_FILE_NAME.tar.gz
Then, I compress the file into a `tar.gz` file. After, it is where the encryption happens. I have a file located somewhere in my server with my GPG password, it is used to encrypt my files using the `gpg` command. The command then returns a "filename.tar.gz.gpg" file, which is then uploaded to Wasabi.
echo "Uploading"
aws s3 cp /root/$NEXTCLOUD_FILE_NAME.tar.gz.gpg s3://backups-cloud/Nextcloud/$NEXTCLOUD_FILE_NAME.tar.gz.gpg --endpoint-url=https://s3.wasabisys.com
echo "Deleting"
rm /root/$NEXTCLOUD_FILE_NAME.tar.gz /root/$NEXTCLOUD_FILE_NAME.tar.gz.gpg
Finally, I upload everything to Wasabi using `awscli` and delete the file, so I keep my filesystem clean.
## Is that it?
This is the basic setup for backups, and it is repeated among all my apps, with few variations
## Dokuwiki
# Dokuwiki
echo "======================================"
echo "Backing up Dokuwiki"
cd /data/docker
DOKUWIKI_FILE_NAME=$(date +"%Y_%m_%d")_dokuwiki_backup
echo "Compressing"
tar czf /root/$DOKUWIKI_FILE_NAME.tar.gz dokuwiki/
echo "Encrypting"
gpg --passphrase-file the/location/of/my/passphrase --batch -c /root/$DOKUWIKI_FILE_NAME.tar.gz
echo "Uploading"
aws s3 cp /root/$DOKUWIKI_FILE_NAME.tar.gz.gpg s3://backups-cloud/Dokuwiki/$DOKUWIKI_FILE_NAME.tar.gz.gpg --endpoint-url=https://s3.wasabisys.com
echo "Deleting"
rm /root/$DOKUWIKI_FILE_NAME.tar.gz /root/$DOKUWIKI_FILE_NAME.tar.gz.gpg
Pretty much the same as the last one, so here is a quick explanation:
* `cd` to a folder
* tar it
* encrypt it with gpg
* upload it to a Wasabi bucket
* delete the local files
## Ghost
# Ghost
echo "======================================"
echo "Backing up Ghost"
cd /root
GHOST_FILE_NAME=$(date +"%Y_%m_%d")_ghost_backup
docker container cp ghost_ghost_1:/var/lib/ghost/ $GHOST_FILE_NAME
docker exec ghost_db_1 /usr/bin/mysqldump -u root --password=my-secure-root-password ghost > /root/$GHOST_FILE_NAME/ghost.sql
echo "Compressing"
tar czf /root/$GHOST_FILE_NAME.tar.gz $GHOST_FILE_NAME/
echo "Encrypting"
gpg --passphrase-file the/location/of/my/passphrase --batch -c /root/$GHOST_FILE_NAME.tar.gz
echo "Uploading"
aws s3 cp /root/$GHOST_FILE_NAME.tar.gz.gpg s3://backups-cloud/Ghost/$GHOST_FILE_NAME.tar.gz.gpg --endpoint-url=https://s3.wasabisys.com
echo "Deleting"
rm -r /root/$GHOST_FILE_NAME.tar.gz $GHOST_FILE_NAME /root/$GHOST_FILE_NAME.tar.gz.gpg
## A few differences!
docker container cp ghost_ghost_1:/var/lib/ghost/ $GHOST_FILE_NAME
docker exec ghost_db_1 /usr/bin/mysqldump -u root --password=my-secure-root-password ghost > /root/$GHOST_FILE_NAME/ghost.sql
Something new! Since on Ghost I didn't mount any volumes, I had to get the files directly from the docker container and then get a DB dump for safekeeping. Nothing too groundbreaking, but worth explaining.
# All done! How do I run it automatically?
Almost done! I just need to run everything automatically, so I can just set it and forget it. Just like before, whenever I want to run something programatically, I will use a cronjob:
0 0 * * 1 sh /opt/backup.sh
This means:
_Please, can you run this script every Monday at 0:00? Thanks, server :_*
# Looking good! Does it work?
Look for yourself :)
![Captura-de-pantalla-de-2019-11-24-19-26-45](/Captura-de-pantalla-de-2019-11-24-19-26-45.png)
<small>Nextcloud</small>
![Captura-de-pantalla-de-2019-11-24-19-28-09](/Captura-de-pantalla-de-2019-11-24-19-28-09.png)
<small>Dokuwiki</small>
![Captura-de-pantalla-de-2019-11-24-19-29-04](/Captura-de-pantalla-de-2019-11-24-19-29-04.png)
<small>Ghost</small>
# Where do we go from here?
I don't know, I only know this project is not over. I have other apps running (Wallabag, Matomo and Commento), but I don't find them as interesting for a new post (of course, if you still want to read about it I will gladly do it).
I hope you all learned from and enjoyed this experience with me because I sure have! I've had amazing feedback from the community and that's what always kept this project on my mind.
A big thank you to [/r/selfhosted](https://reddit.com/r/selfhosted) and more recently [/r/degoogle](https://www.reddit.com/r/degoogle), I learned A LOT from those communities. If you liked these series, you will definitely like those subreddits.
I'm looking to transform all this knowledge to educational talks soon, so if you are in the Montevideo area, stay tuned for a _possible_ meetup! (I know this is a longshot in a country of around 4 million people, but worth trying hehe).
Again, thank you for joining me on this journey and stay tuned! There is more content coming :)

View File

@ -0,0 +1,147 @@
---
title: "How to search in a huge table on Django admin"
url: "/2020/02/17/how-to-search-in-a-huge-table-on-django-admin"
date: 2020-02-17T17:08:00-04:00
lastmod: 2020-04-25T12:35:53-03:00
tags : [ "python", "django", "programming" ]
---
<div class="kg-card-markdown">
Hello everyone!
We all know that the Django admin is a super cool tool for Django. You can check your models, and add/edit/delete records from the tables. If you are familiar with Django, I'm sure you already know about it.
I was given a task: Our client wanted to search in a table by one field. It seems easy enough, right? Well, the tricky part is that the table has **523.803.417 records**.
Wow. **523.803.417 records**.
At least the model was not that complex:
On `models.py`:
class HugeTable(models.Model):
"""Huge table information"""
search_field = models.CharField(max_length=10, db_index=True, unique=True)
is_valid = models.BooleanField(default=True)
def __str__(self):
return self.search_field
So for Django admin, it should be a breeze, right? **WRONG.**
## The process
First, I just added the search field on the admin.py:
On `admin.py`:
class HugeTableAdmin(admin.ModelAdmin):
search_fields = ('search_field', )
admin.site.register(HugeTable, HugeTableAdmin)
And it worked! I had a functioning search field on my admin.
![2020-02-14-154646](/2020-02-14-154646.png)
Only one problem: It took **3mins+** to load the page and **5mins+** to search. But at least it was working, right?
## WTF?
First, let's split the issues:
1. Why was it taking +3mins just to load the page?
2. Why was it taking +5mins to search if the search field was indexed?
I started tackling the first one, and found it quite easily: Django was getting only 100 records at a time, but **it had to calculate the length for the paginator and the "see more" button on the search bar**
![2020-02-14-153605](/2020-02-14-153605.png)
<small>So near, yet so far</small>
## Improving the page load
A quick look at the Django docs told me how to deactivate the "see more" query:
[ModelAdmin.show_full_result_count](https://docs.djangoproject.com/en/2.2/ref/contrib/admin/#django.contrib.admin.ModelAdmin.show_full_result_count)
> Set show_full_result_count to control whether the full count of objects should be displayed on a filtered admin page (e.g. 99 results (103 total)). If this option is set to False, a text like 99 results (Show all) is displayed instead.
On `admin.py`:
class HugeTableAdmin(admin.ModelAdmin):
search_fields = ('search_field', )
show_full_result_count = False
admin.site.register(HugeTable, HugeTableAdmin)
That fixed one problem, but how about the other? It seemed I needed to do my paginator.
Thankfully, I found an _awesome_ post by Haki Benita called ["Optimizing the Django Admin Paginator"](https://hakibenita.com/optimizing-the-django-admin-paginator) that explained exactly that. Since I didn't need to know the records count, I went with the "Dumb" approach:
On `admin.py`:
from django.core.paginator import Paginator
from Django.utils.functional import cached_property
class DumbPaginator(Paginator):
"""
Paginator that does not count the rows in the table.
"""
@cached_property
def count(self):
return 9999999999
class HugeTableAdmin(admin.ModelAdmin):
search_fields = ('search_field', )
show_full_result_count = False
paginator = DumbPaginator
admin.site.register(HugeTable, HugeTableAdmin)
And it worked! The page was loading blazingly fast :) But the search was still **ultra slow**. So let's fix that.
![2020-02-14-153840](/2020-02-14-153840.png)
## Improving the search
I checked A LOT of options. I almost went with [Haystack](https://haystacksearch.org/), but it seemed a bit overkill for what I needed. I finally found this super cool tool: [djangoql](https://github.com/ivelum/djangoql/). It allowed me to search the table by using _sql like_ operations, so I could search by `search_field` and make use of the indexation. So I installed it:
On `settings.py`:
INSTALLED_APPS = [
...
'djangoql',
...
]
On `admin.py`:
from django.core.paginator import Paginator
from django.utils.functional import cached_property
from djangoql.admin import DjangoQLSearchMixin
class DumbPaginator(Paginator):
"""
Paginator that does not count the rows in the table.
"""
@cached_property
def count(self):
return 9999999999
class HugeTableAdmin(DjangoQLSearchMixin, admin.ModelAdmin):
show_full_result_count = False
paginator = DumbPaginator
admin.site.register(HugeTable, HugeTableAdmin)
And it worked! By performing the query:
search_field = "my search query"
I get my results in around 1 second.
![2020-02-14-154418](/2020-02-14-154418.png)
## Is it done?
Yes! Now my client can search by `search_field` on a table of 523.803.417 records, very easily and very quickly.
I'm planning to post more Python/Django things I'm learning by working with this client, so you might want to stay tuned :)

View File

@ -0,0 +1,56 @@
---
title: "My mom was always right | Rant on social media"
date: 2020-04-25T12:35:53-03:00
lastmod: 2020-04-25T12:35:53-03:00
tags : [ "socialmedia", "rant" ]
---
My mom always hated social media. My bother and I always made fun of her because she was always late to all the news. Her main reason against social media was "why would I want everyone to know what I'm doing? And why should I care what they are doing?". I didn't understand at the time, but now I do.
I remember when I was 13 years old social media started ramping up. I created a MySpace account to go with the flow. I won't lie, I liked MySpace a lot: creating a website that "defined me", sharing my music, posting funny pictures and checking my friends' profiles. It all seemed pretty cool and innovative.
Then it came to Facebook, where you couldn't create your own site or share music players like in MySpace, but you had a wall and your friends could leave messages on your wall! How cool was that? You could share pictures, thoughts, opinions, and your friends didn't have to go to your profile to check it out: Facebook had a feed with all your friends' posts, so there was no need to visit their profile just to see what they were doing. It sounded very cool at the moment!
After that, Twitter. Microblogging, 140 chars max (now its 280 chars, double of what it was before), interactions with people all around the internet, you didn't need be friends with someone to send them a tweet or a private message. Discussions, threads, memes...
Instagram. Sharing pictures, stories, following my friends to see their travels, following superstars to see their perfect lifes, ads, paid content.
Stop.
Just stop.
Social media has become too overwhelming in the last couple of years. People are lonelier and more depressed because of social media ([link](https://guilfordjournals.com/doi/10.1521/jscp.2018.37.10.751)). The [FOMO (Fear of missing out)](https://en.wikipedia.org/wiki/Fear_of_missing_out) is at an all-time high.
Now that I know all of this, **why would I want to be part of something that could make me feel anxiety, depression and have self-esteem issues?** That was the question I made to myself around 6 months ago. I consider myself an exaggerated person, so I went full in. The goal was to stop using **all** social media for 1 month and see the results. So here are my thoughts about the experiment
## I feel more relaxed
I don't have the need to open my phone when I'm at a bus stop or just doing nothing. I don't care what my friends are posting, I don't care if an influencer bought 'x' thing or traveled to 'y' place. Before leaving social media, those things had little impact on me and my daily life, so why should I care?
## I have more free time, or time to do more productive stuff
The first week I realized how much time I was wasting using social media, I was wasting between 3 to 4 hours a day in social media, but now I have that time for myself. Now I have built a server for my apartment, I have improved my programming, updated my website, updated and improved my working PC and many other things.
## I can appreciate things a lot more
This decision came at the same time as I had to migrate from Venezuela to Uruguay. So, being in a new country I wanted to visit a lot of new places. I went to museums, parks, beaches (in winter, a bit dumb), monuments and many other touristic attractions. It is funny how I was one of the few that enjoyed the moment instead of being neck-deep in my phone. I was free to enjoy the new city I live in.
## My "reach to my pocket for my phone" tick stopped
I wasn't checking my phone as much as before. I could meet and talk to people without my phone being on the table, and I also realized how rude it is to be ignored because everyone at the table is checking Instagram on their phones.
## But all of this wasn't always pretty
I had to make a lot of changes because I depended on social media for many other things:
* I installed a Feed aggregator on my PC and added sources for all news I want to watch (and also memes lol). I try to keep it with as few sources as possible, when I see news repeating, I delete the least interesting source. The main difference is that **feed aggregators have endings.** I check it once a day and never spend more than 10 mins on it.
* My girlfriend now has to find all the "Instagram business". Being foreigners on a strange land we sometimes need supplies from home. We have found a few by word of mouth, but we had to resource back to Instagram and check with groups of local Venezuelans.
* I have missed some family pictures, but that is easily fixable. My new cousin was born a month ago, and since I don't use any type of social media, I asked my uncle to send me some pictures and I now have a bunch of pictures of the baby, my other cousins, and more family members. Now they even send me pictures without me asking! The conversations have become more intimate than before, where I would just swipe on a feed and "double-tap" to like.
## Conclusions?
My mom was right, as always. The information overload was fun at first, but then it became _overwhelming_. Social media has entered our society as a spy and made us dependent on it, and that's bad. We need to get rid of it.
This experiment started in November 2019 and I can happily say that I left social media for 1 month and never went back. I want to close my Facebook / Instagram accounts (I need to backup my content first) and leave Twitter / LinkedIn because I sometimes use it for work.
I now recommend everyone I know to shut down everything for a while and see how it feels. They might find out that there is a big world out there if they just move their head up and away from their phones.

7
deploy.sh Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/env bash
rm -rf ~/code/personal/blog.rogs.me/public/*
hugo -D
ssh root@cloud.rogs.me "rm -rf /var/www/blog.rogs.me/*"
scp -r ~/code/personal/blog.rogs.me/public/* root@cloud.rogs.me:/var/www/blog.rogs.me
ssh root@cloud.rogs.me "sudo service nginx restart"

BIN
static/.me-small.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
static/5f5af45.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

BIN
static/avatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

BIN
static/cat.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
static/yo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

12
themes/m10c/.editorconfig Normal file
View File

@ -0,0 +1,12 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false

2
themes/m10c/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
public/
exampleSite/resources/

21
themes/m10c/LICENSE.md Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Fabien Casters
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.

62
themes/m10c/README.md Normal file
View File

@ -0,0 +1,62 @@
# m10c theme
![Intro](https://github.com/vaga/hugo-theme-m10c/blob/master/images/cover.png)
A Hugo minimalistic theme for bloggers
## Getting started
### Installation
Create a new Hugo site:
```bash
$ hugo new site [path]
```
Clone this repository into `themes/` directory:
```bash
$ cd [path]
$ git clone https://github.com/vaga/hugo-theme-m10c.git themes/m10c
```
Add this line in the `config.toml` file:
```toml
theme = "m10c"
```
### Configuration
In your `config.toml` file, define the following variables in `params`:
- `author`: Name of the author
- `description`: Short description of the author
- `avatar`: Path of file containing the author avatar image
To add a social link, add the following lines in `params`:
```
[[params.social]]
name = "github"
url = "https://github.com/vaga"
```
To change theme colors, add the following lines in `params`:
```
[params.style]
darkestColor = "#d35050"
darkColor = "#212121"
lightColor = "#f5e3e0"
lightestColor = "#f5f5f5"
primaryColor = "#fff"
```
If you want the above theme colors, you can see the [exampleSite/config.toml](/exampleSite/config.toml) file.
## License
This theme is released under the [**MIT**](/LICENSE.md) License.
## Aknowledgements
- [feather](https://feathericons.com/) - [MIT](https://github.com/feathericons/feather/blob/master/LICENSE)

View File

@ -0,0 +1,55 @@
* {
box-sizing: border-box;
}
html {
line-height: 1.6;
}
body {
margin: 0;
font-family: sans-serif;
background: $dark-color;
color: $light-color;
}
h1, h2, h3, h4, h5, h6 {
color: $lightest-color;
}
a {
color: $primary-color;
transition: color 0.35s;
text-decoration: none;
&:hover {
color: $lightest-color;
}
}
// Browsers seem to use a smaller default font-size with monospaced code
// blocks (like 80% of the size of normal text) and that looks pretty bad with
// small inline code-blocks in the middle of normal text (mainly because of
// the very noticeable difference in x-height). This CSS corrects that problem.
code {
font-family: monospace,monospace;
font-size: 1em;
color: rgba($light-color, .8);
}
pre {
// A larger monospaced block of text (that isn't mixed with normal text)
// generally looks heavier than normal text with the same font size. For this
// reason using a smaller monospaced font size makes sense in this situation.
code {
font-size: .8em;
}
overflow: auto;
}
::selection {
background: rgba($light-color, .25);
}
::-moz-selection {
background: rgba($light-color, .25);
}

View File

@ -0,0 +1,2 @@
// Do not add any CSS to this file in the theme sources.
// This file can be overridden to add project-specific CSS.

View File

@ -0,0 +1,40 @@
.app-header {
padding: 2.5em;
background: $darkest-color;
text-align: center;
}
.app-header-avatar {
max-width: 15rem;
max-height: 15rem;
border-radius: 100%;
border: 0.5rem solid $primary-color;
}
.app-container {
padding: 2.5rem;
}
.app-header-social {
font-size: 2em;
color: $lightest-color;
a {
margin: 0 0.1em;
}
}
@media (min-width: 940px) {
.app-header {
position: fixed;
top: 0;
left: 0;
width: 20rem;
min-height: 100vh;
}
.app-container {
max-width: 65rem;
margin-left: 20rem;
}
}

View File

@ -0,0 +1,7 @@
.error-404 {
text-align: center;
}
.error-404-title {
text-transform: uppercase;
}

View File

@ -0,0 +1,6 @@
.icon {
display: block-inline;
width: 1em;
height: 1em;
vertical-align: -0.125em;
}

View File

@ -0,0 +1,24 @@
.pagination {
display: block;
list-style: none;
padding: 0;
font-size: 0.8em;
text-align: center;
margin: 3em 0;
}
.page-item {
display: inline-block;
.page-link {
display: block;
padding: 0.285em 0.8em;
}
&.active {
.page-link {
color: $lightest-color;
border-radius: 2em;
background: $primary-color;
}
}
}

View File

@ -0,0 +1,17 @@
.post-title {
color: $lightest-color;
}
.post-content {
pre {
border-left: 0.4em solid rgba($primary-color, .8);
padding-left: 1em;
}
img {
max-width: 100%;
}
}
.post-meta {
font-size: 0.8em;
}

View File

@ -0,0 +1,17 @@
.posts-list {
padding: 0;
}
.posts-list-item {
list-style: none;
&:not(:last-child) {
border-bottom: 1px dashed rgba(255, 255, 255, 0.3);
}
padding: 0.4em 0;
}
.posts-list-item-description {
display: block;
font-size: 0.8em;
}

View File

@ -0,0 +1,15 @@
.tag {
display: inline-block;
margin-right: 0.2em;
padding: 0 0.6em;
font-size: 0.9em;
border-radius: 0.2em;
white-space: nowrap;
background: rgba(255, 255, 255, 0.1);
transition: color 0.35s, background 0.35s;
&:hover {
transition: color 0.25s, background 0.05s;
background: rgba(255, 255, 255, 0.3);
}
}

View File

@ -0,0 +1,19 @@
$darkest-color: {{ .Site.Params.style.darkestColor | default "#242930" }};
$dark-color: {{ .Site.Params.style.darkColor | default "#353b43" }};
$light-color: {{ .Site.Params.style.lightColor | default "#afbac4" }};
$lightest-color: {{ .Site.Params.style.lightestColor | default "#ffffff" }};
$primary-color: {{ .Site.Params.style.primaryColor | default "#57cc8a" }};
@import 'base';
@import 'components/app';
@import 'components/error_404';
@import 'components/icon';
@import 'components/pagination';
@import 'components/post';
@import 'components/posts_list';
@import 'components/tag';
// The last 'extra' import can optionally be overridden on a per project
// basis by creating a <HUGO_PROJECT>/assets/css/_extra.scss file.
@import 'extra';

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,39 @@
baseURL = "https://example.com"
title = "Your title"
themesDir = "../.."
theme = "m10c"
paginate = 5
[params]
author = "John Doe"
description = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc vehicula turpis sit amet elit pretium."
[[params.social]]
name = "github"
url = "https://github.com/gohugoio"
[[params.social]]
name = "twitter"
url = "https://twitter.com/gohugoio"
# Brown theme
#[params.style]
# darkestColor = "#463625"
# darkColor = "#2a3738"
# primaryColor = "#eeee6a"
# lightColor = "#96a879"
# lightestColor = "#ffffff"
# Green theme
#[params.style]
# darkestColor = "#315659"
# darkColor = "#253031"
# primaryColor = "#dad865"
# lightColor = "#96a879"
# lightestColor = "#fff"
# Red and black theme
#[params.style]
# darkestColor = "#d35050"
# darkColor = "#212121"
# primaryColor = "#ffffff"
# lightColor = "#a2a2a2"
# lightestColor = "#d3d3d3"

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,332 @@
+++
title = "(Hu)go Template Primer"
tags = ["go", "golang", "templates", "themes", "development"]
date = "2014-04-02"
+++
Hugo uses the excellent [Go][] [html/template][gohtmltemplate] library for
its template engine. It is an extremely lightweight engine that provides a very
small amount of logic. In our experience that it is just the right amount of
logic to be able to create a good static website. If you have used other
template systems from different languages or frameworks you will find a lot of
similarities in Go templates.
This document is a brief primer on using Go templates. The [Go docs][gohtmltemplate]
provide more details.
## Introduction to Go Templates
Go templates provide an extremely simple template language. It adheres to the
belief that only the most basic of logic belongs in the template or view layer.
One consequence of this simplicity is that Go templates parse very quickly.
A unique characteristic of Go templates is they are content aware. Variables and
content will be sanitized depending on the context of where they are used. More
details can be found in the [Go docs][gohtmltemplate].
## Basic Syntax
Golang templates are HTML files with the addition of variables and
functions.
**Go variables and functions are accessible within {{ }}**
Accessing a predefined variable "foo":
{{ foo }}
**Parameters are separated using spaces**
Calling the add function with input of 1, 2:
{{ add 1 2 }}
**Methods and fields are accessed via dot notation**
Accessing the Page Parameter "bar"
{{ .Params.bar }}
**Parentheses can be used to group items together**
{{ if or (isset .Params "alt") (isset .Params "caption") }} Caption {{ end }}
## Variables
Each Go template has a struct (object) made available to it. In hugo each
template is passed either a page or a node struct depending on which type of
page you are rendering. More details are available on the
[variables](/layout/variables) page.
A variable is accessed by referencing the variable name.
<title>{{ .Title }}</title>
Variables can also be defined and referenced.
{{ $address := "123 Main St."}}
{{ $address }}
## Functions
Go template ship with a few functions which provide basic functionality. The Go
template system also provides a mechanism for applications to extend the
available functions with their own. [Hugo template
functions](/layout/functions) provide some additional functionality we believe
are useful for building websites. Functions are called by using their name
followed by the required parameters separated by spaces. Template
functions cannot be added without recompiling hugo.
**Example:**
{{ add 1 2 }}
## Includes
When including another template you will pass to it the data it will be
able to access. To pass along the current context please remember to
include a trailing dot. The templates location will always be starting at
the /layout/ directory within Hugo.
**Example:**
{{ template "chrome/header.html" . }}
## Logic
Go templates provide the most basic iteration and conditional logic.
### Iteration
Just like in Go, the Go templates make heavy use of range to iterate over
a map, array or slice. The following are different examples of how to use
range.
**Example 1: Using Context**
{{ range array }}
{{ . }}
{{ end }}
**Example 2: Declaring value variable name**
{{range $element := array}}
{{ $element }}
{{ end }}
**Example 2: Declaring key and value variable name**
{{range $index, $element := array}}
{{ $index }}
{{ $element }}
{{ end }}
### Conditionals
If, else, with, or, & and provide the framework for handling conditional
logic in Go Templates. Like range, each statement is closed with `end`.
Go Templates treat the following values as false:
* false
* 0
* any array, slice, map, or string of length zero
**Example 1: If**
{{ if isset .Params "title" }}<h4>{{ index .Params "title" }}</h4>{{ end }}
**Example 2: If -> Else**
{{ if isset .Params "alt" }}
{{ index .Params "alt" }}
{{else}}
{{ index .Params "caption" }}
{{ end }}
**Example 3: And & Or**
{{ if and (or (isset .Params "title") (isset .Params "caption")) (isset .Params "attr")}}
**Example 4: With**
An alternative way of writing "if" and then referencing the same value
is to use "with" instead. With rebinds the context `.` within its scope,
and skips the block if the variable is absent.
The first example above could be simplified as:
{{ with .Params.title }}<h4>{{ . }}</h4>{{ end }}
**Example 5: If -> Else If**
{{ if isset .Params "alt" }}
{{ index .Params "alt" }}
{{ else if isset .Params "caption" }}
{{ index .Params "caption" }}
{{ end }}
## Pipes
One of the most powerful components of Go templates is the ability to
stack actions one after another. This is done by using pipes. Borrowed
from unix pipes, the concept is simple, each pipeline's output becomes the
input of the following pipe.
Because of the very simple syntax of Go templates, the pipe is essential
to being able to chain together function calls. One limitation of the
pipes is that they only can work with a single value and that value
becomes the last parameter of the next pipeline.
A few simple examples should help convey how to use the pipe.
**Example 1 :**
{{ if eq 1 1 }} Same {{ end }}
is the same as
{{ eq 1 1 | if }} Same {{ end }}
It does look odd to place the if at the end, but it does provide a good
illustration of how to use the pipes.
**Example 2 :**
{{ index .Params "disqus_url" | html }}
Access the page parameter called "disqus_url" and escape the HTML.
**Example 3 :**
{{ if or (or (isset .Params "title") (isset .Params "caption")) (isset .Params "attr")}}
Stuff Here
{{ end }}
Could be rewritten as
{{ isset .Params "caption" | or isset .Params "title" | or isset .Params "attr" | if }}
Stuff Here
{{ end }}
## Context (aka. the dot)
The most easily overlooked concept to understand about Go templates is that {{ . }}
always refers to the current context. In the top level of your template this
will be the data set made available to it. Inside of a iteration it will have
the value of the current item. When inside of a loop the context has changed. .
will no longer refer to the data available to the entire page. If you need to
access this from within the loop you will likely want to set it to a variable
instead of depending on the context.
**Example:**
{{ $title := .Site.Title }}
{{ range .Params.tags }}
<li> <a href="{{ $baseurl }}/tags/{{ . | urlize }}">{{ . }}</a> - {{ $title }} </li>
{{ end }}
Notice how once we have entered the loop the value of {{ . }} has changed. We
have defined a variable outside of the loop so we have access to it from within
the loop.
# Hugo Parameters
Hugo provides the option of passing values to the template language
through the site configuration (for sitewide values), or through the meta
data of each specific piece of content. You can define any values of any
type (supported by your front matter/config format) and use them however
you want to inside of your templates.
## Using Content (page) Parameters
In each piece of content you can provide variables to be used by the
templates. This happens in the [front matter](/content/front-matter).
An example of this is used in this documentation site. Most of the pages
benefit from having the table of contents provided. Sometimes the TOC just
doesn't make a lot of sense. We've defined a variable in our front matter
of some pages to turn off the TOC from being displayed.
Here is the example front matter:
```
---
title: "Permalinks"
date: "2013-11-18"
aliases:
- "/doc/permalinks/"
groups: ["extras"]
groups_weight: 30
notoc: true
---
```
Here is the corresponding code inside of the template:
{{ if not .Params.notoc }}
<div id="toc" class="well col-md-4 col-sm-6">
{{ .TableOfContents }}
</div>
{{ end }}
## Using Site (config) Parameters
In your top-level configuration file (eg, `config.yaml`) you can define site
parameters, which are values which will be available to you in chrome.
For instance, you might declare:
```yaml
params:
CopyrightHTML: "Copyright &#xA9; 2013 John Doe. All Rights Reserved."
TwitterUser: "spf13"
SidebarRecentLimit: 5
```
Within a footer layout, you might then declare a `<footer>` which is only
provided if the `CopyrightHTML` parameter is provided, and if it is given,
you would declare it to be HTML-safe, so that the HTML entity is not escaped
again. This would let you easily update just your top-level config file each
January 1st, instead of hunting through your templates.
```
{{if .Site.Params.CopyrightHTML}}<footer>
<div class="text-center">{{.Site.Params.CopyrightHTML | safeHtml}}</div>
</footer>{{end}}
```
An alternative way of writing the "if" and then referencing the same value
is to use "with" instead. With rebinds the context `.` within its scope,
and skips the block if the variable is absent:
```
{{with .Site.Params.TwitterUser}}<span class="twitter">
<a href="https://twitter.com/{{.}}" rel="author">
<img src="/images/twitter.png" width="48" height="48" title="Twitter: {{.}}"
alt="Twitter"></a>
</span>{{end}}
```
Finally, if you want to pull "magic constants" out of your layouts, you can do
so, such as in this example:
```
<nav class="recent">
<h1>Recent Posts</h1>
<ul>{{range first .Site.Params.SidebarRecentLimit .Site.Recent}}
<li><a href="{{.RelPermalink}}">{{.Title}}</a></li>
{{end}}</ul>
</nav>
```
[go]: https://golang.org/
[gohtmltemplate]: https://golang.org/pkg/html/template/

View File

@ -0,0 +1,78 @@
+++
title = "Getting Started with Hugo"
tags = ["go", "golang", "hugo", "development", "tutorial", "docs", "configuration"]
date = "2014-04-02"
+++
## Step 1. Install Hugo
Go to [Hugo releases](https://github.com/spf13/hugo/releases) and download the
appropriate version for your OS and architecture.
Save it somewhere specific as we will be using it in the next step.
More complete instructions are available at [Install Hugo](https://gohugo.io/getting-started/installing/)
## Step 2. Build the Docs
Hugo has its own example site which happens to also be the documentation site
you are reading right now.
Follow the following steps:
1. Clone the [Hugo repository](http://github.com/spf13/hugo)
2. Go into the repo
3. Run hugo in server mode and build the docs
4. Open your browser to http://localhost:1313
Corresponding pseudo commands:
git clone https://github.com/spf13/hugo
cd hugo
/path/to/where/you/installed/hugo server --source=./docs
> 29 pages created
> 0 tags index created
> in 27 ms
> Web Server is available at http://localhost:1313
> Press ctrl+c to stop
Once you've gotten here, follow along the rest of this page on your local build.
## Step 3. Change the docs site
Stop the Hugo process by hitting Ctrl+C.
Now we are going to run hugo again, but this time with hugo in watch mode.
/path/to/hugo/from/step/1/hugo server --source=./docs --watch
> 29 pages created
> 0 tags index created
> in 27 ms
> Web Server is available at http://localhost:1313
> Watching for changes in /Users/spf13/Code/hugo/docs/content
> Press ctrl+c to stop
Open your [favorite editor](http://vim.spf13.com) and change one of the source
content pages. How about changing this very file to *fix the typo*. How about changing this very file to *fix the typo*.
Content files are found in `docs/content/`. Unless otherwise specified, files
are located at the same relative location as the url, in our case
`docs/content/overview/quickstart.md`.
Change and save this file.. Notice what happened in your terminal.
> Change detected, rebuilding site
> 29 pages created
> 0 tags index created
> in 26 ms
Refresh the browser and observe that the typo is now fixed.
Notice how quick that was. Try to refresh the site before it's finished building. I double dare you.
Having nearly instant feedback enables you to have your creativity flow without waiting for long builds.
## Step 4. Have fun
The best way to learn something is to play with it.

View File

@ -0,0 +1,151 @@
+++
title = "Migrate to Hugo from Jekyll"
tags = ["hugo", "jekyll"]
date = "2014-03-10"
+++
## Move static content to `static`
Jekyll has a rule that any directory not starting with `_` will be copied as-is to the `_site` output. Hugo keeps all static content under `static`. You should therefore move it all there.
With Jekyll, something that looked like
<root>/
▾ images/
logo.png
should become
<root>/
▾ static/
▾ images/
logo.png
Additionally, you'll want any files that should reside at the root (such as `CNAME`) to be moved to `static`.
## Create your Hugo configuration file
Hugo can read your configuration as JSON, YAML or TOML. Hugo supports parameters custom configuration too. Refer to the [Hugo configuration documentation](/overview/configuration/) for details.
## Set your configuration publish folder to `_site`
The default is for Jekyll to publish to `_site` and for Hugo to publish to `public`. If, like me, you have [`_site` mapped to a git submodule on the `gh-pages` branch](http://blog.blindgaenger.net/generate_github_pages_in_a_submodule.html), you'll want to do one of two alternatives:
1. Change your submodule to point to map `gh-pages` to public instead of `_site` (recommended).
git submodule deinit _site
git rm _site
git submodule add -b gh-pages git@github.com:your-username/your-repo.git public
2. Or, change the Hugo configuration to use `_site` instead of `public`.
{
..
"publishdir": "_site",
..
}
## Convert Jekyll templates to Hugo templates
That's the bulk of the work right here. The documentation is your friend. You should refer to [Jekyll's template documentation](http://jekyllrb.com/docs/templates/) if you need to refresh your memory on how you built your blog and [Hugo's template](/layout/templates/) to learn Hugo's way.
As a single reference data point, converting my templates for [heyitsalex.net](http://heyitsalex.net/) took me no more than a few hours.
## Convert Jekyll plugins to Hugo shortcodes
Jekyll has [plugins](http://jekyllrb.com/docs/plugins/); Hugo has [shortcodes](/doc/shortcodes/). It's fairly trivial to do a port.
### Implementation
As an example, I was using a custom [`image_tag`](https://github.com/alexandre-normand/alexandre-normand/blob/74bb12036a71334fdb7dba84e073382fc06908ec/_plugins/image_tag.rb) plugin to generate figures with caption when running Jekyll. As I read about shortcodes, I found Hugo had a nice built-in shortcode that does exactly the same thing.
Jekyll's plugin:
module Jekyll
class ImageTag < Liquid::Tag
@url = nil
@caption = nil
@class = nil
@link = nil
// Patterns
IMAGE_URL_WITH_CLASS_AND_CAPTION =
IMAGE_URL_WITH_CLASS_AND_CAPTION_AND_LINK = /(\w+)(\s+)((https?:\/\/|\/)(\S+))(\s+)"(.*?)"(\s+)->((https?:\/\/|\/)(\S+))(\s*)/i
IMAGE_URL_WITH_CAPTION = /((https?:\/\/|\/)(\S+))(\s+)"(.*?)"/i
IMAGE_URL_WITH_CLASS = /(\w+)(\s+)((https?:\/\/|\/)(\S+))/i
IMAGE_URL = /((https?:\/\/|\/)(\S+))/i
def initialize(tag_name, markup, tokens)
super
if markup =~ IMAGE_URL_WITH_CLASS_AND_CAPTION_AND_LINK
@class = $1
@url = $3
@caption = $7
@link = $9
elsif markup =~ IMAGE_URL_WITH_CLASS_AND_CAPTION
@class = $1
@url = $3
@caption = $7
elsif markup =~ IMAGE_URL_WITH_CAPTION
@url = $1
@caption = $5
elsif markup =~ IMAGE_URL_WITH_CLASS
@class = $1
@url = $3
elsif markup =~ IMAGE_URL
@url = $1
end
end
def render(context)
if @class
source = "<figure class='#{@class}'>"
else
source = "<figure>"
end
if @link
source += "<a href=\"#{@link}\">"
end
source += "<img src=\"#{@url}\">"
if @link
source += "</a>"
end
source += "<figcaption>#{@caption}</figcaption>" if @caption
source += "</figure>"
source
end
end
end
Liquid::Template.register_tag('image', Jekyll::ImageTag)
is written as this Hugo shortcode:
<!-- image -->
<figure {{ with .Get "class" }}class="{{.}}"{{ end }}>
{{ with .Get "link"}}<a href="{{.}}">{{ end }}
<img src="{{ .Get "src" }}" {{ if or (.Get "alt") (.Get "caption") }}alt="{{ with .Get "alt"}}{{.}}{{else}}{{ .Get "caption" }}{{ end }}"{{ end }} />
{{ if .Get "link"}}</a>{{ end }}
{{ if or (or (.Get "title") (.Get "caption")) (.Get "attr")}}
<figcaption>{{ if isset .Params "title" }}
{{ .Get "title" }}{{ end }}
{{ if or (.Get "caption") (.Get "attr")}}<p>
{{ .Get "caption" }}
{{ with .Get "attrlink"}}<a href="{{.}}"> {{ end }}
{{ .Get "attr" }}
{{ if .Get "attrlink"}}</a> {{ end }}
</p> {{ end }}
</figcaption>
{{ end }}
</figure>
<!-- image -->
### Usage
I simply changed:
{% image full http://farm5.staticflickr.com/4136/4829260124_57712e570a_o_d.jpg "One of my favorite touristy-type photos. I secretly waited for the good light while we were "having fun" and took this. Only regret: a stupid pole in the top-left corner of the frame I had to clumsily get rid of at post-processing." ->http://www.flickr.com/photos/alexnormand/4829260124/in/set-72157624547713078/ %}
to this (this example uses a slightly extended version named `fig`, different than the built-in `figure`):
{{%/* fig class="full" src="http://farm5.staticflickr.com/4136/4829260124_57712e570a_o_d.jpg" title="One of my favorite touristy-type photos. I secretly waited for the good light while we were having fun and took this. Only regret: a stupid pole in the top-left corner of the frame I had to clumsily get rid of at post-processing." link="http://www.flickr.com/photos/alexnormand/4829260124/in/set-72157624547713078/" */%}}
As a bonus, the shortcode named parameters are, arguably, more readable.
## Finishing touches
### Fix content
Depending on the amount of customization that was done with each post with Jekyll, this step will require more or less effort. There are no hard and fast rules here except that `hugo server --watch` is your friend. Test your changes and fix errors as needed.
### Clean up
You'll want to remove the Jekyll configuration at this point. If you have anything else that isn't used, delete it.
## A practical example in a diff
[Hey, it's Alex](http://heyitsalex.net/) was migrated in less than a _father-with-kids day_ from Jekyll to Hugo. You can see all the changes (and screw-ups) by looking at this [diff](https://github.com/alexandre-normand/alexandre-normand/compare/869d69435bd2665c3fbf5b5c78d4c22759d7613a...b7f6605b1265e83b4b81495423294208cc74d610).

View File

@ -0,0 +1,11 @@
+++
title = "Test 1"
tags = ["test"]
date = "1012-01-01"
+++
Test 1
I am referencing a footnote[^1]
[^1]: I am the footnote

View File

@ -0,0 +1,7 @@
+++
title = "Test 2"
tags = ["test"]
date = "1012-01-02"
+++
Test 2

View File

@ -0,0 +1,7 @@
+++
title = "Test 3"
tags = ["test"]
date = "1012-01-03"
+++
Test 3

View File

@ -0,0 +1,7 @@
+++
title = "Test 4"
tags = ["test"]
date = "1012-01-04"
+++
Test 4

View File

@ -0,0 +1,7 @@
+++
title = "Test 5"
tags = ["test"]
date = "1012-01-05"
+++
Test 5

View File

@ -0,0 +1,7 @@
+++
title = "Test 6"
tags = ["test"]
date = "1012-01-06"
+++
Test 6

View File

@ -0,0 +1,7 @@
+++
title = "Test 7"
tags = ["test"]
date = "1012-01-07"
+++
Test 7

View File

@ -0,0 +1,7 @@
+++
title = "Test 8"
tags = ["test"]
date = "1012-01-08"
+++
Test 8

View File

@ -0,0 +1,7 @@
+++
title = "Test 9"
tags = ["test"]
date = "1012-01-09"
+++
Test 9

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

BIN
themes/m10c/images/tn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

View File

@ -0,0 +1,7 @@
{{ define "main" }}
<div class="error-404">
<h1 class="error-404-title">O{{ partial "icon.html" (dict "ctx" $ "name" "frown") }}ps... </h1>
<p>The link you followed may be broken, or the page may have been removed.</p>
<a href="{{ "/" | relURL }}">Go home {{ partial "icon.html" (dict "ctx" $ "name" "arrow-right") }}</a>
</div>
{{ end }}

View File

@ -0,0 +1,54 @@
<!doctype html>
<html lang="{{ .Site.LanguageCode | default "en-us" }}">
<head>
<title>{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} // {{ .Site.Title }}{{ end }}</title>
<meta charset="utf-8" />
{{ hugo.Generator }}
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="author" content="{{ .Site.Params.author | default "John Doe" }}" />
<meta name="description" content="{{ if .IsHome }}{{ .Site.Params.description }}{{ else }}{{ .Description }}{{ end }}" />
{{ $style := resources.Get "css/main.scss" | resources.ExecuteAsTemplate "css/main.scss" . | resources.ToCSS | resources.Minify | resources.Fingerprint -}}
<link rel="stylesheet" href="{{ $style.Permalink }}" />
<link rel='shortcut icon' type='image/x-icon' href='/favicon.ico' />
{{ template "_internal/google_analytics.html" . }}
{{ template "_internal/twitter_cards.html" . }}
{{ template "_internal/opengraph.html" . }}
<!-- Matomo -->
<script type="text/javascript">
var _paq = window._paq || [];
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(["setDocumentTitle", document.domain + "/" + document.title]);
_paq.push(["setDomains", ["*.blog.rogs.me"]]);
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="//analytics.rogs.me/";
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', '2']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})();
</script>
<!-- End Matomo Code -->
</head>
<body>
<header class="app-header">
<a href="{{ .Site.BaseURL }}"><img class="app-header-avatar" src="{{ .Site.Params.avatar | default "avatar.jpg" | relURL }}" alt="{{ .Site.Params.author | default "John Doe" }}" /></a>
<h1>{{ .Site.Title }}</h1>
<p>{{ .Site.Params.description | default "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc vehicula turpis sit amet elit pretium." }}</p>
<div class="app-header-social">
{{ range .Site.Params.social }}
<a target="_blank" href="{{ .url }}" rel="noreferrer noopener">{{ partial "icon.html" (dict "ctx" $ "name" .name ) }}</a>
{{ end }}
</div>
</header>
<main class="app-container">
{{ block "main" . }}
{{ .Content }}
{{ end }}
</main>
</body>
</html>

View File

@ -0,0 +1,17 @@
{{ define "main" }}
<article>
<h1>{{ .Title }}</h1>
<ul class="posts-list">
{{ range $index, $element := .Paginator.Pages }}
<li class="posts-list-item">
<a class="posts-list-item-title" href="{{ .Permalink }}">{{ .Title }}</a>
<span class="posts-list-item-description">
{{ partial "icon.html" (dict "ctx" $ "name" "clock") }} {{ .ReadingTime }} min read -
{{ .PublishDate.Format "Jan 2, 2006" }}
</span>
</li>
{{ end }}
</ul>
{{ partial "pagination.html" $ }}
</article>
{{ end }}

View File

@ -0,0 +1,34 @@
{{ define "main" }}
<article class="post">
<header class="post-header">
<h1 class ="post-title">{{ .Title }}</h1>
<div class="post-meta">
<div>
{{ partial "icon.html" (dict "ctx" $ "name" "calendar") }}
{{ .PublishDate.Format "Jan 2, 2006" }}
</div>
<div>
{{ partial "icon.html" (dict "ctx" $ "name" "clock") }}
{{ .ReadingTime }} min read
</div>
{{- with .Params.tags -}}
<div>
{{ partial "icon.html" (dict "ctx" $ "name" "tag") }}
{{ range . -}}
{{- with $.Site.GetPage (printf "/%s/%s" "tags" . ) -}}
<a class="tag" href="{{ .Permalink }}">{{ .Title }}</a>
{{- end -}}
{{- end -}}
</div>
{{- end -}}
</div>
</header>
<div class="post-content">
{{ .Content }}
</div>
<div class="post-footer">
<script defer src="https://commento.rogs.me/js/commento.js"></script>
<div id="commento"></div>
</div>
</article>
{{ end }}

View File

@ -0,0 +1,16 @@
{{ define "main" }}
<article>
<h1>{{ .Title }}</h1>
<ul class="posts-list">
{{ range .Data.Terms.Alphabetical }}
<li class="posts-list-item">
<a class="posts-list-item-title" href="{{ .Page.Permalink }}">
{{ .Page.Title }}
</a>
{{ .Count }}
</li>
{{ end }}
</ul>
{{ partial "pagination.html" $ }}
</article>
{{ end }}

View File

@ -0,0 +1,12 @@
{{- if isset .ctx.Site.Data.m10c.icons .name -}}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-{{ .name }}">
<title>{{ .name }}</title>
{{ safeHTML (index .ctx.Site.Data.m10c.icons .name) }}
</svg>
{{- else -}}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-link">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
{{- end -}}

View File

@ -0,0 +1,27 @@
{{ with $.Paginator }}
{{ if gt .TotalPages 1 }}
<ul class="pagination">
{{ with .Prev }}
<li class="page-item">
<a class="page-link" href="{{ .URL }}">
{{ partial "icon.html" (dict "ctx" $ "name" "arrow-left") }}
</a>
</li>
{{ end }}
{{ range .Pagers }}
<li class="page-item{{ if eq .PageNumber $.Paginator.PageNumber }} active{{ end }}">
<a class="page-link" href="{{ .URL }}">
{{ .PageNumber }}
</a>
</li>
{{ end }}
{{ with .Next }}
<li class="page-item">
<a class="page-link" href="{{ .URL }}">
{{ partial "icon.html" (dict "ctx" $ "name" "arrow-right") }}
</a>
</li>
{{ end }}
</ul>
{{ end }}
{{ end }}

View File

@ -0,0 +1 @@
*{box-sizing:border-box}html{line-height:1.6}body{margin:0;font-family:sans-serif;background:#353b43;color:#afbac4}h1,h2,h3,h4,h5,h6{color:#fff}a{color:#57cc8a;text-decoration:none}a:hover{color:#fff;transition:color .8s}pre{overflow:auto}.app-header{padding:2.5em;background:#242930;text-align:center}.app-header-avatar{max-width:15rem;max-height:15rem;border-radius:100%;border:.5rem solid #57cc8a}.app-container{padding:2.5rem}.app-header-social{font-size:2em;color:#fff}.app-header-social a{margin:0 .1em}@media(min-width:940px){.app-header{position:fixed;top:0;left:0;width:20rem;min-height:100vh}.app-container{max-width:65rem;margin-left:20rem}}.error-404{text-align:center}.error-404-title{text-transform:uppercase}.icon{display:block-inline;width:1em;height:1em;vertical-align:-.125em}.pagination{display:block;list-style:none;padding:0;font-size:.8em;text-align:center;margin:3em 0}.page-item{display:inline-block}.page-item .page-link{display:block;padding:.285em .8em}.page-item.active .page-link{color:#fff;border-radius:2em;background:#57cc8a}.post-title{color:#fff}.post-content pre{border-left:.4em solid rgba(87,204,138,.8);padding-left:1em}.post-meta{font-size:.8em}.posts-list{padding:0}.posts-list-item{list-style:none;border-bottom:1px dashed rgba(255,255,255,.3);padding:.4em 0}.posts-list-item-description{display:block;font-size:.8em}.tag{display:inline-block;margin-right:.2em;padding:0 .6em;font-size:.9em;border-radius:.2em;white-space:nowrap;background:rgba(255,255,255,.1);transition:background .8s}.tag:hover{background:rgba(255,255,255,.3)}

View File

@ -0,0 +1 @@
{"Target":"css/main.min.f90f5edd436ec7b74ad05479a05705770306911f721193e7845948fb07fe1335.css","MediaType":"text/css","Data":{"Integrity":"sha256-+Q9e3UNux7dK0FR5oFcFdwMGkR9yEZPnhFlI+wf+EzU="}}

View File

@ -0,0 +1,24 @@
#!/bin/sh
FEATHER_PATH=`dirname "$0"`/feather
# Clone feather repository
git clone --depth=1 git@github.com:feathericons/feather.git $FEATHER_PATH
pushd $FEATHER_PATH
# Install dependencies
yarn
mkdir dist
# Generate icons.json
npx babel-node bin/build-icons-json.js
# Copy icons.json into theme data
cp dist/icons.json ../../data/m10c/icons.json
popd
# Remove the repository
rm -rf $FEATHER_PATH

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

17
themes/m10c/theme.toml Normal file
View File

@ -0,0 +1,17 @@
name = "m10c"
license = "MIT"
licenselink = "https://github.com/vaga/hugo-theme-m10c/blob/master/LICENSE.md"
description = "A minimalistic (m10c) theme for bloggers"
homepage = "https://github.com/vaga/hugo-theme-m10c"
tags = ["blog", "minimalistic", "minimal", "responsive", "dark"]
features = [
"minimalistic blog",
"social icons",
"editable colors"
]
min_version = 0.55
[author]
name = "vaga"
homepage = "https://vaga.io"