blog: Zola site, nginx image, Forgejo deploy pipeline
This commit is contained in:
commit
a4b677f898
23 changed files with 561 additions and 0 deletions
104
.forgejo/workflows/deploy.yaml
Normal file
104
.forgejo/workflows/deploy.yaml
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
name: Build and Deploy to Production
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Same job as in test.yaml (non-main branches); duplicated so a broken
|
||||||
|
# build blocks deploy — Forgejo has no cross-workflow needs.
|
||||||
|
check:
|
||||||
|
runs-on: nix-latest
|
||||||
|
steps:
|
||||||
|
- name: Prepare container for actions
|
||||||
|
run: |
|
||||||
|
echo "experimental-features = nix-command flakes" >> /etc/nix/nix.conf
|
||||||
|
nix-env -iA nixpkgs.nodejs_22
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build site
|
||||||
|
run: nix develop -c zola build
|
||||||
|
|
||||||
|
build:
|
||||||
|
runs-on: arch-latest
|
||||||
|
needs: check
|
||||||
|
container:
|
||||||
|
image: gcr.io/kaniko-project/executor:debug
|
||||||
|
outputs:
|
||||||
|
image_tag: ${{ steps.build_image.outputs.image_tag }}
|
||||||
|
steps:
|
||||||
|
- name: Build and push Docker Image using Kaniko
|
||||||
|
id: build_image
|
||||||
|
run: |
|
||||||
|
# Tags
|
||||||
|
SHORT_SHA=${GITHUB_SHA::8}
|
||||||
|
IMAGE_BASE=${{ secrets.FORGEJO_REGISTRY }}/${{ secrets.FORGEJO_USER }}/blog
|
||||||
|
IMAGE_TAG=${IMAGE_BASE}:${SHORT_SHA}
|
||||||
|
IMAGE_LATEST=${IMAGE_BASE}:latest
|
||||||
|
|
||||||
|
# Auth Conf
|
||||||
|
mkdir -p /kaniko/.docker
|
||||||
|
echo "{\"auths\":{\"${{ secrets.FORGEJO_REGISTRY }}\":{\"auth\":\"$(echo -n ${{ secrets.FORGEJO_USER }}:${{ secrets.FORGEJO_TOKEN }} | base64)\"}}}" > /kaniko/.docker/config.json
|
||||||
|
|
||||||
|
# Build & push
|
||||||
|
/kaniko/executor \
|
||||||
|
--context=git://${{ secrets.FORGEJO_REGISTRY }}/${{ github.repository }}.git \
|
||||||
|
--git=branch=${{ github.ref_name }} \
|
||||||
|
--destination=$IMAGE_TAG \
|
||||||
|
--destination=$IMAGE_LATEST
|
||||||
|
|
||||||
|
# Output the specific tag for deployment
|
||||||
|
echo "image_tag=${IMAGE_TAG}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
runs-on: arch-latest
|
||||||
|
needs: build
|
||||||
|
steps:
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
pacman -Syu --noconfirm nodejs openssh rsync
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Deploy docker-compose.yml
|
||||||
|
uses: easingthemes/ssh-deploy@v5.1.0
|
||||||
|
with:
|
||||||
|
SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||||
|
REMOTE_HOST: ${{ secrets.DEPLOY_IP }}
|
||||||
|
REMOTE_USER: ${{ secrets.DEPLOY_USER }}
|
||||||
|
SOURCE: "docker-compose.yml"
|
||||||
|
TARGET: "/opt/blog/"
|
||||||
|
|
||||||
|
- name: Deploy and update container
|
||||||
|
uses: appleboy/ssh-action@v1.2.3
|
||||||
|
env:
|
||||||
|
NEW_IMAGE: ${{ needs.build.outputs.image_tag }}
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.DEPLOY_IP }}
|
||||||
|
username: ${{ secrets.DEPLOY_USER }}
|
||||||
|
key: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||||
|
envs: NEW_IMAGE
|
||||||
|
script: |
|
||||||
|
cd /opt/blog
|
||||||
|
|
||||||
|
sed -i "s|image:.*|image: $NEW_IMAGE|" docker-compose.yml
|
||||||
|
|
||||||
|
docker pull $NEW_IMAGE
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Wait for the image HEALTHCHECK to report healthy.
|
||||||
|
for i in $(seq 1 18); do
|
||||||
|
status=$(docker inspect --format '{{.State.Health.Status}}' blog 2>/dev/null || echo missing)
|
||||||
|
if [ "$status" = "healthy" ]; then
|
||||||
|
echo "Deployment successful!"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
echo "blog never became healthy (last status: $status)" >&2
|
||||||
|
docker logs --tail 50 blog
|
||||||
|
exit 1
|
||||||
21
.forgejo/workflows/test.yaml
Normal file
21
.forgejo/workflows/test.yaml
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
name: Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches-ignore:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: nix-latest
|
||||||
|
steps:
|
||||||
|
- name: Prepare container for actions
|
||||||
|
run: |
|
||||||
|
echo "experimental-features = nix-command flakes" >> /etc/nix/nix.conf
|
||||||
|
nix-env -iA nixpkgs.nodejs_22
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build site
|
||||||
|
run: nix develop -c zola build
|
||||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
public/
|
||||||
|
.env
|
||||||
21
CLAUDE.md
Normal file
21
CLAUDE.md
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
# blog.asxp.io — static blog
|
||||||
|
|
||||||
|
Zola static site, built in CI into an nginx container, served behind Traefik on the same prod host as asxp.io. Mirrors the conventions of the sibling `../asxpio` repo (Kaniko build, SSH compose deploy, health-wait); see that repo's CLAUDE.md for the pipeline details.
|
||||||
|
|
||||||
|
## Writing a post
|
||||||
|
|
||||||
|
Add `content/<slug>.md` with TOML front matter (`title`, `date`, optional `description`), push to `main`. CI rebuilds and deploys. Local preview: `nix develop -c zola serve` → http://127.0.0.1:1111.
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- **Zola** — version pinned in `Dockerfile` (`ghcr.io/getzola/zola`, no `:latest` tag exists); the `flake.nix` dev shell tracks nixpkgs independently. Bump the Dockerfile pin manually.
|
||||||
|
- Multi-stage `Dockerfile`: `zola build` → `nginx:alpine` serving the generated `public/`. nginx config in `nginx.conf` (404 page + short static cache).
|
||||||
|
- `docker-compose.yml`: Traefik labels for `blog.asxp.io`, joins only the external `traefik` network — no storage-stack dependency, fully static, no `.env`.
|
||||||
|
- Deploy dir on prod: `/opt/blog`. Secrets used (shared with asxpio): `DEPLOY_IP/USER/SSH_KEY`, `FORGEJO_REGISTRY/USER/TOKEN`. Nothing else.
|
||||||
|
- `templates/` are hand-written Tera (no theme); `static/style.css` reuses the asxp.io dark palette (`#121212` bg, Open Sans + Hack).
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- Same hard rule as the main site: **never use the words "consulting" or "advisory"** in posts or copy — "services", "engineering", "implementation" instead.
|
||||||
|
- Blog content is fully public; no entity details beyond what asxp.io already shows (no banking, no registration number).
|
||||||
|
- Atom feed at `/atom.xml` (`generate_feeds = true` in `config.toml`).
|
||||||
14
Dockerfile
Normal file
14
Dockerfile
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Zola publishes no :latest tag; bump the pin manually (flake.nix tracks nixpkgs separately).
|
||||||
|
FROM ghcr.io/getzola/zola:v0.22.1 AS build
|
||||||
|
|
||||||
|
COPY . /project
|
||||||
|
WORKDIR /project
|
||||||
|
RUN ["zola", "build"]
|
||||||
|
|
||||||
|
FROM nginx:1.28-alpine
|
||||||
|
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY --from=build /project/public /usr/share/nginx/html
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
|
||||||
|
CMD wget -qO /dev/null http://127.0.0.1/ || exit 1
|
||||||
14
config.toml
Normal file
14
config.toml
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
base_url = "https://blog.asxp.io"
|
||||||
|
title = "asxp.io · blog"
|
||||||
|
description = "Notes on DevOps and infrastructure engineering by Sergei Poljanski."
|
||||||
|
default_language = "en"
|
||||||
|
author = "Sergei Poljanski"
|
||||||
|
|
||||||
|
compile_sass = false
|
||||||
|
build_search_index = false
|
||||||
|
generate_feeds = true
|
||||||
|
feed_filenames = ["atom.xml"]
|
||||||
|
|
||||||
|
[markdown.highlighting]
|
||||||
|
enabled = true
|
||||||
|
theme = "ayu-dark"
|
||||||
5
content/_index.md
Normal file
5
content/_index.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
+++
|
||||||
|
sort_by = "date"
|
||||||
|
template = "index.html"
|
||||||
|
page_template = "page.html"
|
||||||
|
+++
|
||||||
11
content/hello-world.md
Normal file
11
content/hello-world.md
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
+++
|
||||||
|
title = "Hello, world"
|
||||||
|
date = 2026-07-16
|
||||||
|
description = "First post: what this blog is for."
|
||||||
|
+++
|
||||||
|
|
||||||
|
Short posts on DevOps and infrastructure engineering: NixOS, Kubernetes, Docker,
|
||||||
|
CI/CD, and the occasional war story from running small self-hosted stacks.
|
||||||
|
|
||||||
|
The blog itself is [Zola](https://www.getzola.org/), built in CI into an nginx
|
||||||
|
container and served behind Traefik, next to [asxp.io](https://asxp.io).
|
||||||
21
docker-compose.yml
Normal file
21
docker-compose.yml
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
services:
|
||||||
|
blog:
|
||||||
|
# Image will be dynamically updated during deployment
|
||||||
|
# Format: registry.example.com/user/blog:sha_tag
|
||||||
|
build: .
|
||||||
|
image: blog:latest
|
||||||
|
container_name: blog
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- traefik
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.blog.rule=Host(`blog.asxp.io`)"
|
||||||
|
- "traefik.http.routers.blog.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.blog.tls=true"
|
||||||
|
- "traefik.http.routers.blog.tls.certresolver=letsencrypt"
|
||||||
|
- "traefik.http.services.blog.loadbalancer.server.port=80"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
traefik:
|
||||||
|
external: true
|
||||||
27
flake.lock
generated
Normal file
27
flake.lock
generated
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1784120854,
|
||||||
|
"narHash": "sha256-KesHgItiZPgGX740axSiQLcIQ8D24MDqNpkKYWIek8k=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "753cc8a3a87467296ddd1fa93f0cc3e81120ee46",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
25
flake.nix
Normal file
25
flake.nix
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
description = "blog.asxp.io — static blog (Zola)";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs }:
|
||||||
|
let
|
||||||
|
system = "x86_64-linux";
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShells.${system}.default = pkgs.mkShell {
|
||||||
|
buildInputs = [
|
||||||
|
pkgs.zola
|
||||||
|
];
|
||||||
|
|
||||||
|
shellHook = ''
|
||||||
|
echo "blog dev shell ready"
|
||||||
|
echo " zola serve - live-reload preview on http://127.0.0.1:1111"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
15
nginx.conf
Normal file
15
nginx.conf
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
|
||||||
|
error_page 404 /404.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~* \.(css|png|svg|xml|ico)$ {
|
||||||
|
expires 1h;
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
static/fonts/ibm-plex-mono-latin-400-normal.woff2
Normal file
BIN
static/fonts/ibm-plex-mono-latin-400-normal.woff2
Normal file
Binary file not shown.
BIN
static/fonts/ibm-plex-sans-latin-400-italic.woff2
Normal file
BIN
static/fonts/ibm-plex-sans-latin-400-italic.woff2
Normal file
Binary file not shown.
BIN
static/fonts/ibm-plex-sans-latin-400-normal.woff2
Normal file
BIN
static/fonts/ibm-plex-sans-latin-400-normal.woff2
Normal file
Binary file not shown.
BIN
static/fonts/ibm-plex-sans-latin-600-normal.woff2
Normal file
BIN
static/fonts/ibm-plex-sans-latin-600-normal.woff2
Normal file
Binary file not shown.
BIN
static/fonts/ibm-plex-sans-latin-700-normal.woff2
Normal file
BIN
static/fonts/ibm-plex-sans-latin-700-normal.woff2
Normal file
Binary file not shown.
BIN
static/hedgehog.png
Normal file
BIN
static/hedgehog.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
183
static/style.css
Normal file
183
static/style.css
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
/* Self-hosted fonts (no CDN). Latin subsets, same files as asxp.io. */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'IBM Plex Sans';
|
||||||
|
src: url('/fonts/ibm-plex-sans-latin-400-normal.woff2') format('woff2');
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'IBM Plex Sans';
|
||||||
|
src: url('/fonts/ibm-plex-sans-latin-400-italic.woff2') format('woff2');
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: italic;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'IBM Plex Sans';
|
||||||
|
src: url('/fonts/ibm-plex-sans-latin-600-normal.woff2') format('woff2');
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'IBM Plex Sans';
|
||||||
|
src: url('/fonts/ibm-plex-sans-latin-700-normal.woff2') format('woff2');
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'IBM Plex Mono';
|
||||||
|
src: url('/fonts/ibm-plex-mono-latin-400-normal.woff2') format('woff2');
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Design tokens — kept in sync with asxp.io's public/style.css (source of truth) */
|
||||||
|
:root {
|
||||||
|
--bg: #121212;
|
||||||
|
--surface: #1A1A1A;
|
||||||
|
--surface-alt: #222222;
|
||||||
|
--surface-hover: #333333;
|
||||||
|
--border-subtle: #1E1E1E;
|
||||||
|
--border: #2A2A2A;
|
||||||
|
--border-strong: #3A3A3A;
|
||||||
|
--text: #AAAAAA;
|
||||||
|
--text-bright: #EEEEEE;
|
||||||
|
--text-strong: #DDDDDD;
|
||||||
|
--text-soft: #CCCCCC;
|
||||||
|
--text-link: #BBBBBB;
|
||||||
|
--text-dim: #999999;
|
||||||
|
--text-dimmer: #888888;
|
||||||
|
--text-faint: #777777;
|
||||||
|
--text-muted: #666666;
|
||||||
|
--text-ghost: #555555;
|
||||||
|
--text-hover: #FFFFFF;
|
||||||
|
--link-underline: #444444;
|
||||||
|
--danger: #C97070;
|
||||||
|
--danger-border: #4A1F1F;
|
||||||
|
--danger-bg: #2A1414;
|
||||||
|
--pending: #C9A86A;
|
||||||
|
--pending-border: #4A3E1F;
|
||||||
|
--pending-bg: #2A2414;
|
||||||
|
--paid: #7AB97A;
|
||||||
|
--paid-border: #234A23;
|
||||||
|
--paid-bg: #142A14;
|
||||||
|
--font-sans: 'IBM Plex Sans', sans-serif;
|
||||||
|
--font-mono: 'IBM Plex Mono', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
max-width: 720px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 40px 20px 80px;
|
||||||
|
}
|
||||||
|
.site-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding-bottom: 12px;
|
||||||
|
margin-bottom: 36px;
|
||||||
|
}
|
||||||
|
.site-title {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: var(--text-bright);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.site-header nav a {
|
||||||
|
color: var(--text-faint);
|
||||||
|
text-decoration: none;
|
||||||
|
margin-left: 18px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.site-header nav a:hover,
|
||||||
|
.site-title:hover {
|
||||||
|
color: var(--text-bright);
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: var(--text-soft);
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: var(--text-hover);
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-bright);
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
margin: 0 0 4px;
|
||||||
|
}
|
||||||
|
article h2 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-strong);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding-bottom: 6px;
|
||||||
|
margin: 32px 0 14px;
|
||||||
|
}
|
||||||
|
.post-meta {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin: 0 0 28px;
|
||||||
|
}
|
||||||
|
ul.post-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
ul.post-list li {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: baseline;
|
||||||
|
padding: 6px 0;
|
||||||
|
}
|
||||||
|
ul.post-list time {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.92em;
|
||||||
|
background: var(--surface-alt);
|
||||||
|
padding: 1px 5px;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
background: #1B1B1B;
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
border: 1px solid #020202;
|
||||||
|
margin: 14px 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
pre code {
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
blockquote {
|
||||||
|
border-left: 3px solid var(--border);
|
||||||
|
margin: 14px 0;
|
||||||
|
padding: 0 0 0 16px;
|
||||||
|
color: var(--text-dimmer);
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-ghost);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 20px 0 40px;
|
||||||
|
}
|
||||||
8
templates/404.html
Normal file
8
templates/404.html
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}404 · {{ config.title }}{% endblock title %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>404</h1>
|
||||||
|
<p>No such page. <a href="/">Back to the index.</a></p>
|
||||||
|
{% endblock content %}
|
||||||
59
templates/base.html
Normal file
59
templates/base.html
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
||||||
|
<title>{% block title %}{{ config.title }}{% endblock title %}</title>
|
||||||
|
<meta name="description" content="{% block description %}{{ config.description }}{% endblock description %}" />
|
||||||
|
<meta name="author" content="Sergei Poljanski" />
|
||||||
|
<link rel="canonical" href="{{ current_url | default(value=config.base_url) }}" />
|
||||||
|
|
||||||
|
<meta property="og:type" content="{% block og_type %}website{% endblock og_type %}" />
|
||||||
|
<meta property="og:site_name" content="{{ config.title }}" />
|
||||||
|
<meta property="og:title" content="{% block og_title %}{{ config.title }}{% endblock og_title %}" />
|
||||||
|
<meta property="og:url" content="{{ current_url | default(value=config.base_url) }}" />
|
||||||
|
{% block extra_head %}{% endblock extra_head %}
|
||||||
|
|
||||||
|
<link rel="icon" type="image/png" href="/hedgehog.png" sizes="32x32" />
|
||||||
|
<link rel="apple-touch-icon" href="/hedgehog.png" />
|
||||||
|
<link rel="alternate" type="application/atom+xml" title="{{ config.title }}" href="{{ get_url(path='atom.xml') }}" />
|
||||||
|
|
||||||
|
<!-- Fonts are self-hosted; see @font-face in style.css -->
|
||||||
|
<link rel="preload" href="/fonts/ibm-plex-sans-latin-400-normal.woff2" as="font" type="font/woff2" crossorigin />
|
||||||
|
<link rel="preload" href="/fonts/ibm-plex-sans-latin-600-normal.woff2" as="font" type="font/woff2" crossorigin />
|
||||||
|
<link rel="stylesheet" href="/style.css" />
|
||||||
|
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Blog",
|
||||||
|
"@id": "{{ config.base_url | safe }}/#blog",
|
||||||
|
"url": "{{ config.base_url | safe }}",
|
||||||
|
"name": "{{ config.title }}",
|
||||||
|
"description": "{{ config.description }}",
|
||||||
|
"inLanguage": "en",
|
||||||
|
"author": { "@id": "https://asxp.io/#person" },
|
||||||
|
"publisher": { "@id": "https://asxp.io/#organization" }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<header class="site-header">
|
||||||
|
<a class="site-title" href="/">blog.asxp.io</a>
|
||||||
|
<nav>
|
||||||
|
<a href="https://asxp.io">asxp.io</a>
|
||||||
|
<a href="{{ get_url(path='atom.xml') }}">feed</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
{% block content %}{% endblock content %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<div class="footer-line">
|
||||||
|
IE Sergei Poljanski · Tbilisi, Georgia
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
12
templates/index.html
Normal file
12
templates/index.html
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<ul class="post-list">
|
||||||
|
{% for page in section.pages %}
|
||||||
|
<li>
|
||||||
|
<time datetime="{{ page.date }}">{{ page.date | date(format="%Y-%m-%d") }}</time>
|
||||||
|
<a href="{{ page.permalink }}">{{ page.title }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endblock content %}
|
||||||
19
templates/page.html
Normal file
19
templates/page.html
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ page.title }} · {{ config.title }}{% endblock title %}
|
||||||
|
{% block og_type %}article{% endblock og_type %}
|
||||||
|
{% block og_title %}{{ page.title }}{% endblock og_title %}
|
||||||
|
{% block description %}{{ page.description | default(value=config.description) }}{% endblock description %}
|
||||||
|
{% block extra_head %}
|
||||||
|
<meta property="article:published_time" content="{{ page.date }}" />
|
||||||
|
{% endblock extra_head %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<article>
|
||||||
|
<h1>{{ page.title }}</h1>
|
||||||
|
<p class="post-meta">
|
||||||
|
<time datetime="{{ page.date }}">{{ page.date | date(format="%Y-%m-%d") }}</time>
|
||||||
|
</p>
|
||||||
|
{{ page.content | safe }}
|
||||||
|
</article>
|
||||||
|
{% endblock content %}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue