Murad Library
RESEARCH#md

RESEARCH

Soledade

Projeto-Soledade.md

research·#MD·Projeto-Soledade.md
Date
Reading
20 min read

Soledade

Tutorial completo para criar um blog textual estilo Nightfall/Nex/NPS

Versão: 1.0
Data: 2026-05-31
Autor do projeto: Pablo Murad
Objetivo: criar um blog minimalista, textual, acessível por navegador e também por terminal, inspirado em sistemas como Nightfall City, Nex, Gopher, Gemini e web antiga.


1. O que é o Soledade?

Soledade é um sistema de blog textual minimalista. Ele não tenta ser WordPress, Medium, Substack ou rede social. A ideia é outra:

  • páginas simples;
  • posts escritos em Markdown;
  • publicação por terminal;
  • acesso via navegador comum;
  • leitura via protocolo textual próprio;
  • pouca dependência;
  • estética de internet antiga;
  • controle total do servidor.

O sistema terá três camadas:

HTTPS / navegador     -> site público normal
TCP porta 1900        -> leitura textual estilo Nex
TCP porta 1915        -> postagem textual estilo NPS

O fluxo ideal:

Você escreve um post -> envia por terminal -> servidor salva -> site é reconstruído -> post aparece na web e no terminal

2. Inspiração: Nightfall, Nex e NPS

A inspiração mais direta vem da Nightfall City:

No Nightfall, o protocolo Nex serve documentos por TCP na porta 1900. O cliente envia um caminho, o servidor responde com texto ou binário e fecha a conexão.

Exemplo conceitual:

printf "nex/info/specification.txt\n" | nc nightfall.city 1900

O NPS é o serviço de postagem. Ele escuta na porta 1915. Você conecta, envia um caminho, uma chave e o conteúdo. Uma linha contendo apenas . encerra a transmissão.

Exemplo conceitual:

shore/usuario/index
CHAVE_SECRETA
conteúdo da página
.

O Soledade usará uma ideia parecida, mas adaptada para blog pessoal.


3. O que vamos construir

O Soledade terá:

  1. Gerador estático em Python: transforma Markdown em HTML.
  2. Site público servido por nginx ou Caddy: https://blog.seudominio.com/.
  3. Servidor de leitura textual: porta 1900, para ler conteúdo via terminal.
  4. Servidor de postagem: porta 1915, para publicar/editar posts via terminal.
  5. Token secreto: uma chave simples para autorizar publicações.
  6. systemd: para manter os serviços rodando no Linux.
  7. Firewall: para abrir apenas as portas necessárias.

4. Ferramentas recomendadas

Sistema operacional

Recomendado:

  • Debian 12 ou Debian 13
  • Ubuntu Server 24.04 LTS
  • VPS simples com 1 GB RAM já basta

Links úteis:

Linguagem

Use Python para a primeira versão. Não complique.

Links:

Markdown

Opções boas:

Para o Soledade, recomendo Python-Markdown pela simplicidade.

Servidor web

Duas opções:

nginx

Mais clássico, robusto, comum em servidores Linux.

Caddy

Mais simples para HTTPS automático. Excelente para projetos pequenos.

Minha recomendação prática:

  • Se você quer controle e padrão de mercado: nginx.
  • Se quer velocidade e menos dor com certificado: Caddy.

HTTPS

Se usar nginx:

Se usar Caddy, ele já cuida disso automaticamente.

Segurança

Deploy e edição

Geradores estáticos prontos, caso você desista de fazer do zero

Se em algum momento você quiser algo pronto:

Mas para o projeto Soledade, vamos fazer do zero porque o objetivo é aprender e ter controle.


5. Arquitetura final

/opt/soledade/
├── content/
│   ├── index.md
│   ├── about.md
│   └── posts/
│       └── primeiro-post.md
├── public/
│   ├── index.html
│   ├── about/
│   │   └── index.html
│   └── posts/
│       └── primeiro-post/
│           └── index.html
├── soledade/
│   ├── build.py
│   ├── nex_server.py
│   ├── post_server.py
│   └── config.py
├── templates/
│   └── base.html
├── secrets/
│   └── token.txt
├── logs/
└── venv/

Função de cada pasta

content/    -> textos originais em Markdown
public/     -> HTML gerado para a web
soledade/   -> código Python do sistema
templates/  -> layout HTML
secrets/    -> token privado de publicação
logs/       -> logs simples
venv/       -> ambiente virtual Python

6. Preparando o servidor

Entre no seu servidor Debian/Ubuntu:

ssh usuario@SEU_IP

Atualize:

sudo apt update
sudo apt upgrade -y

Instale pacotes básicos:

sudo apt install -y python3 python3-venv python3-pip nginx ufw git curl unzip

Crie usuário dedicado:

sudo adduser --system --group --home /opt/soledade soledade

Crie diretório:

sudo mkdir -p /opt/soledade
sudo chown -R soledade:soledade /opt/soledade

Entre como usuário do projeto:

sudo -u soledade -H bash
cd /opt/soledade

7. Criando a estrutura

mkdir -p content/posts public soledade templates secrets logs
python3 -m venv venv
source venv/bin/activate
pip install Markdown

Crie o token secreto:

openssl rand -hex 32 > secrets/token.txt
chmod 600 secrets/token.txt
cat secrets/token.txt

Guarde esse token. Ele será sua senha de publicação.


8. Arquivo de configuração

Crie:

nano soledade/config.py

Conteúdo:

from pathlib import Path

BASE_DIR = Path("/opt/soledade")
CONTENT_DIR = BASE_DIR / "content"
PUBLIC_DIR = BASE_DIR / "public"
TEMPLATE_FILE = BASE_DIR / "templates" / "base.html"
TOKEN_FILE = BASE_DIR / "secrets" / "token.txt"
LOG_DIR = BASE_DIR / "logs"

SITE_NAME = "Soledade"
SITE_DESCRIPTION = "Um blog textual, quieto e feito à mão."
BASE_URL = "https://blog.seudominio.com"

NEX_HOST = "0.0.0.0"
NEX_PORT = 1900

POST_HOST = "0.0.0.0"
POST_PORT = 1915

MAX_POST_BYTES = 128 * 1024

Troque https://blog.seudominio.com pelo seu domínio real.


9. Template HTML

Crie:

nano templates/base.html

Conteúdo:

<!doctype html>
<html lang="pt-BR">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{{ title }} - {{ site_name }}</title>
  <meta name="description" content="{{ description }}">
  <style>
    :root {
      color-scheme: dark;
      --bg: #0b0d10;
      --fg: #d8d4c8;
      --muted: #8b867a;
      --link: #9ccfd8;
      --border: #2a2f35;
      --code: #15191f;
    }
    body {
      margin: 0;
      background: var(--bg);
      color: var(--fg);
      font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
      line-height: 1.65;
    }
    main { max-width: 760px; margin: 0 auto; padding: 48px 20px 80px; }
    header { border-bottom: 1px solid var(--border); margin-bottom: 32px; padding-bottom: 16px; }
    footer { border-top: 1px solid var(--border); margin-top: 48px; padding-top: 16px; color: var(--muted); font-size: 0.9rem; }
    a { color: var(--link); text-decoration: none; }
    a:hover { text-decoration: underline; }
    h1, h2, h3 { line-height: 1.25; }
    pre, code { background: var(--code); }
    pre { padding: 16px; overflow-x: auto; border: 1px solid var(--border); }
    code { padding: 2px 4px; }
    blockquote { border-left: 3px solid var(--border); margin-left: 0; padding-left: 16px; color: var(--muted); }
    .site-title { font-size: 1.4rem; font-weight: bold; }
    .site-description { color: var(--muted); }
  </style>
</head>
<body>
  <main>
    <header>
      <div class="site-title"><a href="/">{{ site_name }}</a></div>
      <div class="site-description">{{ site_description }}</div>
    </header>
    <article>
      {{ content }}
    </article>
    <footer>
      Feito em texto. Servido por silêncio. Lido por navegador ou terminal.
    </footer>
  </main>
</body>
</html>

10. Conteúdo inicial

Crie:

nano content/index.md

Conteúdo:

# Soledade

Bem-vindo ao Soledade.

Este é um blog textual, mínimo e feito à mão.

Não há feed infinito.  
Não há algoritmo.  
Não há ruído.

Apenas páginas, caminhos e memória.

## Portas

- [Sobre](/about/)
- [Posts](/posts/)

## Leitura pelo terminal

Quando o protocolo textual estiver ativo:

```bash
printf "index.md\n" | ncat blog.seudominio.com 1900
```

Crie:

nano content/about.md

Conteúdo:

# Sobre

Soledade é um experimento de publicação textual.

A web ficou pesada demais. Este lugar escolhe o contrário:

- texto;
- calma;
- permanência;
- controle;
- caminhos simples.

[Voltar](/)

Crie pasta de posts:

mkdir -p content/posts
nano content/posts/primeiro-post.md

Conteúdo:

# Primeiro post

Este é o primeiro post do Soledade.

Um blog não precisa começar grande. Precisa apenas começar.

A forma importa: quando a ferramenta é simples, o texto fica mais honesto.

11. Gerador estático

Crie:

nano soledade/build.py

Conteúdo:

#!/usr/bin/env python3
from __future__ import annotations

import html
import shutil
from pathlib import Path

import markdown

from config import CONTENT_DIR, PUBLIC_DIR, TEMPLATE_FILE, SITE_NAME, SITE_DESCRIPTION


def extract_title(md_text: str, fallback: str) -> str:
    for line in md_text.splitlines():
        if line.startswith("# "):
            return line[2:].strip()
    return fallback


def render_template(title: str, content_html: str) -> str:
    template = TEMPLATE_FILE.read_text(encoding="utf-8")
    return (
        template
        .replace("{{ title }}", html.escape(title))
        .replace("{{ site_name }}", html.escape(SITE_NAME))
        .replace("{{ site_description }}", html.escape(SITE_DESCRIPTION))
        .replace("{{ description }}", html.escape(SITE_DESCRIPTION))
        .replace("{{ content }}", content_html)
    )


def md_to_html(md_text: str) -> str:
    return markdown.markdown(
        md_text,
        extensions=["extra", "toc", "sane_lists"],
        output_format="html5",
    )


def output_path_for(source: Path) -> Path:
    relative = source.relative_to(CONTENT_DIR)
    if relative.name == "index.md":
        return PUBLIC_DIR / relative.with_suffix(".html")
    if relative.suffix == ".md":
        return PUBLIC_DIR / relative.with_suffix("") / "index.html"
    return PUBLIC_DIR / relative


def build_page(source: Path) -> None:
    md_text = source.read_text(encoding="utf-8")
    title = extract_title(md_text, source.stem.replace("-", " ").title())
    content_html = md_to_html(md_text)
    html_text = render_template(title, content_html)
    target = output_path_for(source)
    target.parent.mkdir(parents=True, exist_ok=True)
    target.write_text(html_text, encoding="utf-8")
    print(f"built {source} -> {target}")


def build_posts_index() -> None:
    posts_dir = CONTENT_DIR / "posts"
    if not posts_dir.exists():
        return
    links = []
    for post in sorted(posts_dir.glob("*.md"), reverse=True):
        text = post.read_text(encoding="utf-8")
        title = extract_title(text, post.stem.replace("-", " ").title())
        url = f"/posts/{post.stem}/"
        links.append(f"- [{title}]({url})")
    md_text = "# Posts\n\n" + "\n".join(links) + "\n"
    html_text = render_template("Posts", md_to_html(md_text))
    target = PUBLIC_DIR / "posts" / "index.html"
    target.parent.mkdir(parents=True, exist_ok=True)
    target.write_text(html_text, encoding="utf-8")
    print(f"built posts index -> {target}")


def copy_static_files() -> None:
    static_dir = CONTENT_DIR / "static"
    if static_dir.exists():
        target = PUBLIC_DIR / "static"
        if target.exists():
            shutil.rmtree(target)
        shutil.copytree(static_dir, target)


def main() -> None:
    PUBLIC_DIR.mkdir(parents=True, exist_ok=True)
    for source in CONTENT_DIR.rglob("*.md"):
        build_page(source)
    build_posts_index()
    copy_static_files()


if __name__ == "__main__":
    main()

Torne executável:

chmod +x soledade/build.py

Teste:

cd /opt/soledade/soledade
../venv/bin/python build.py
find /opt/soledade/public -type f

12. Servidor web com nginx

Saia do usuário soledade:

exit

Crie configuração nginx:

sudo nano /etc/nginx/sites-available/soledade

Conteúdo:

server {
    listen 80;
    listen [::]:80;
    server_name blog.seudominio.com;
    root /opt/soledade/public;
    index index.html;
    location / {
        try_files $uri $uri/ =404;
    }
    access_log /var/log/nginx/soledade.access.log;
    error_log /var/log/nginx/soledade.error.log;
}

Ative:

sudo ln -s /etc/nginx/sites-available/soledade /etc/nginx/sites-enabled/soledade
sudo nginx -t
sudo systemctl reload nginx

Abra:

http://blog.seudominio.com/

13. HTTPS com Certbot

Instale:

sudo apt install -y certbot python3-certbot-nginx

Execute:

sudo certbot --nginx -d blog.seudominio.com

Teste renovação:

sudo certbot renew --dry-run

14. Alternativa com Caddy

Se preferir Caddy em vez de nginx, a configuração é bem menor.

Instale conforme documentação oficial:

Caddyfile:

blog.seudominio.com {
    root * /opt/soledade/public
    file_server
}

Caddy é excelente porque faz HTTPS automático.


15. Servidor de leitura textual: porta 1900

Agora vamos criar o equivalente minimalista do Nex.

Crie:

sudo -u soledade -H bash
cd /opt/soledade
nano soledade/nex_server.py

Conteúdo:

#!/usr/bin/env python3
from __future__ import annotations

import socketserver
from pathlib import Path

from config import CONTENT_DIR, NEX_HOST, NEX_PORT

WELCOME = """Soledade textual server

Try:
  index.md
  about.md
  posts/primeiro-post.md

"""


def safe_path(raw: str) -> Path | None:
    raw = raw.strip().lstrip("/")
    if not raw:
        raw = "index.md"
    candidate = (CONTENT_DIR / raw).resolve()
    root = CONTENT_DIR.resolve()
    try:
        candidate.relative_to(root)
    except ValueError:
        return None
    return candidate


class NexHandler(socketserver.BaseRequestHandler):
    def handle(self) -> None:
        try:
            data = self.request.recv(4096)
            path_text = data.decode("utf-8", errors="replace").strip()
            if not path_text:
                self.request.sendall(WELCOME.encode("utf-8"))
                return
            path = safe_path(path_text)
            if path is None:
                self.request.sendall(b"403 forbidden\n")
                return
            if path.is_dir():
                index = path / "index.md"
                if index.exists():
                    path = index
                else:
                    entries = sorted(p.name + ("/" if p.is_dir() else "") for p in path.iterdir())
                    self.request.sendall(("\n".join(entries) + "\n").encode("utf-8"))
                    return
            if not path.exists() or not path.is_file():
                self.request.sendall(b"404 not found\n")
                return
            self.request.sendall(path.read_bytes())
        except Exception as exc:
            self.request.sendall(f"500 error: {exc}\n".encode("utf-8", errors="replace"))


class ThreadingTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
    allow_reuse_address = True
    daemon_threads = True


def main() -> None:
    with ThreadingTCPServer((NEX_HOST, NEX_PORT), NexHandler) as server:
        print(f"Soledade Nex-like server listening on {NEX_HOST}:{NEX_PORT}")
        server.serve_forever()


if __name__ == "__main__":
    main()

Teste localmente:

cd /opt/soledade/soledade
../venv/bin/python nex_server.py

Em outro terminal:

printf "index.md\n" | nc localhost 1900

Se estiver no Windows:

"index.md" | ncat SEU_DOMINIO 1900

16. Servidor de postagem: porta 1915

Agora vem o equivalente do NPS.

Crie:

nano /opt/soledade/soledade/post_server.py

Conteúdo:

#!/usr/bin/env python3
from __future__ import annotations

import datetime as dt
import re
import socketserver
import subprocess

from config import BASE_DIR, CONTENT_DIR, TOKEN_FILE, LOG_DIR, POST_HOST, POST_PORT, MAX_POST_BYTES

VALID_PATH = re.compile(r"^[a-zA-Z0-9_./-]+$")

HELP = """Soledade Posting Service

To publish:

posts/meu-post.md
TOKEN
# Meu post
Conteudo
.

Special commands:

CREATE_DIR
DELETE

"""


def load_token() -> str:
    return TOKEN_FILE.read_text(encoding="utf-8").strip()


def log(message: str) -> None:
    LOG_DIR.mkdir(parents=True, exist_ok=True)
    now = dt.datetime.utcnow().isoformat(timespec="seconds") + "Z"
    with (LOG_DIR / "post_server.log").open("a", encoding="utf-8") as f:
        f.write(f"[{now}] {message}\n")


def safe_content_path(raw_path: str):
    raw_path = raw_path.strip().lstrip("/")
    if not raw_path or ".." in raw_path or not VALID_PATH.match(raw_path):
        return None
    candidate = (CONTENT_DIR / raw_path).resolve()
    root = CONTENT_DIR.resolve()
    try:
        candidate.relative_to(root)
    except ValueError:
        return None
    return candidate


def rebuild_site() -> tuple[bool, str]:
    cmd = [str(BASE_DIR / "venv" / "bin" / "python"), str(BASE_DIR / "soledade" / "build.py")]
    result = subprocess.run(cmd, cwd=str(BASE_DIR / "soledade"), capture_output=True, text=True, timeout=30)
    output = (result.stdout + result.stderr).strip()
    return result.returncode == 0, output


class PostHandler(socketserver.BaseRequestHandler):
    def handle(self) -> None:
        try:
            self.request.settimeout(20)
            data = bytearray()
            while True:
                chunk = self.request.recv(4096)
                if not chunk:
                    break
                data.extend(chunk)
                if len(data) > MAX_POST_BYTES:
                    self.request.sendall(b"413 payload too large\n")
                    return
                if b"\n.\n" in data or data.endswith(b"\n.\r\n"):
                    break

            text = data.decode("utf-8", errors="replace")
            lines = text.replace("\r\n", "\n").split("\n")
            if len(lines) < 3:
                self.request.sendall(HELP.encode("utf-8"))
                return

            target_raw = lines[0].strip()
            token = lines[1].strip()
            content_lines = []
            for line in lines[2:]:
                if line == ".":
                    break
                content_lines.append(line)

            if token != load_token():
                log(f"auth failed for {target_raw} from {self.client_address[0]}")
                self.request.sendall(b"403 invalid token\n")
                return

            target = safe_content_path(target_raw)
            if target is None:
                self.request.sendall(b"400 invalid path\n")
                return

            body = "\n".join(content_lines).rstrip() + "\n"

            if body.strip() == "CREATE_DIR":
                target.mkdir(parents=True, exist_ok=True)
                log(f"created dir {target}")
                self.request.sendall(b"200 directory created\n")
                return

            if body.strip() == "DELETE":
                if target.is_dir():
                    self.request.sendall(b"400 refusing to delete directory\n")
                    return
                if target.exists():
                    target.unlink()
                    ok, output = rebuild_site()
                    log(f"deleted {target}; rebuild={ok}")
                    self.request.sendall(f"200 deleted\n{output}\n".encode("utf-8", errors="replace"))
                    return
                self.request.sendall(b"404 file not found\n")
                return

            if not target.name.endswith(".md"):
                self.request.sendall(b"400 only .md files are allowed\n")
                return

            target.parent.mkdir(parents=True, exist_ok=True)
            target.write_text(body, encoding="utf-8")
            ok, output = rebuild_site()
            if ok:
                log(f"published {target}")
                self.request.sendall(f"200 published\n{output}\n".encode("utf-8", errors="replace"))
            else:
                log(f"rebuild failed for {target}: {output}")
                self.request.sendall(f"500 rebuild failed\n{output}\n".encode("utf-8", errors="replace"))
        except Exception as exc:
            log(f"error: {exc}")
            self.request.sendall(f"500 error: {exc}\n".encode("utf-8", errors="replace"))


class ThreadingTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
    allow_reuse_address = True
    daemon_threads = True


def main() -> None:
    with ThreadingTCPServer((POST_HOST, POST_PORT), PostHandler) as server:
        print(f"Soledade Posting Service listening on {POST_HOST}:{POST_PORT}")
        server.serve_forever()


if __name__ == "__main__":
    main()

Teste:

cd /opt/soledade/soledade
../venv/bin/python post_server.py

Em outro terminal:

TOKEN=$(cat /opt/soledade/secrets/token.txt)
cat <<EOF | nc localhost 1915
posts/teste.md
$TOKEN
# Teste

Publicado via terminal.
.
EOF

17. systemd para manter os serviços vivos

Saia do usuário soledade:

exit

Crie serviço do leitor textual:

sudo nano /etc/systemd/system/soledade-nex.service

Conteúdo:

[Unit]
Description=Soledade Nex-like Read Server
After=network.target

[Service]
Type=simple
User=soledade
Group=soledade
WorkingDirectory=/opt/soledade/soledade
ExecStart=/opt/soledade/venv/bin/python /opt/soledade/soledade/nex_server.py
Restart=always
RestartSec=3
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
ReadWritePaths=/opt/soledade

[Install]
WantedBy=multi-user.target

Crie serviço do publicador:

sudo nano /etc/systemd/system/soledade-post.service

Conteúdo:

[Unit]
Description=Soledade Posting Service
After=network.target

[Service]
Type=simple
User=soledade
Group=soledade
WorkingDirectory=/opt/soledade/soledade
ExecStart=/opt/soledade/venv/bin/python /opt/soledade/soledade/post_server.py
Restart=always
RestartSec=3
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ReadWritePaths=/opt/soledade

[Install]
WantedBy=multi-user.target

Ative:

sudo systemctl daemon-reload
sudo systemctl enable --now soledade-nex.service
sudo systemctl enable --now soledade-post.service

Verifique:

sudo systemctl status soledade-nex.service
sudo systemctl status soledade-post.service

Logs:

journalctl -u soledade-nex.service -f
journalctl -u soledade-post.service -f

18. Firewall

Abra SSH, HTTP, HTTPS, Nex e postagem:

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 1900/tcp
sudo ufw allow 1915/tcp
sudo ufw enable
sudo ufw status verbose

Se quiser ser mais seguro, deixe a porta 1915 aberta apenas para seu IP:

sudo ufw delete allow 1915/tcp
sudo ufw allow from SEU_IP_PUBLICO to any port 1915 proto tcp

Essa é a escolha mais sensata se o blog for só seu.


19. Publicando do Windows com Ncat

Você já tem Ncat instalado se instalou Nmap:

ncat --version

Para publicar:

ncat blog.seudominio.com 1915

Digite:

posts/minha-noite.md
SEU_TOKEN
# Minha noite

Texto publicado pelo terminal no Windows.
.

Para ler:

"posts/minha-noite.md" | ncat blog.seudominio.com 1900

Se quiser guardar o token em arquivo local no Windows:

Set-Content -Path "$HOME\.soledade-token" -Value "SEU_TOKEN"

Publicação usando arquivo:

$token = Get-Content "$HOME\.soledade-token"
@"
posts/post-via-powershell.md
$token
# Post via PowerShell

Funciona.
.
"@ | ncat blog.seudominio.com 1915

20. Cliente simples de publicação em PowerShell

Crie soledade-publish.ps1:

param(
    [Parameter(Mandatory=$true)]
    [string]$Path,
    [Parameter(Mandatory=$true)]
    [string]$File,
    [string]$HostName = "blog.seudominio.com",
    [int]$Port = 1915,
    [string]$TokenFile = "$HOME\.soledade-token"
)

if (!(Test-Path $File)) {
    Write-Error "Arquivo nao encontrado: $File"
    exit 1
}
if (!(Test-Path $TokenFile)) {
    Write-Error "Token nao encontrado: $TokenFile"
    exit 1
}

$token = Get-Content $TokenFile -Raw
$token = $token.Trim()
$content = Get-Content $File -Raw

$payload = @"
$Path
$token
$content
.
"@

$payload | ncat $HostName $Port

Uso:

.\soledade-publish.ps1 -Path "posts/meu-post.md" -File ".\meu-post.md"

21. Cliente simples de publicação em Bash

Crie soledade-publish:

#!/usr/bin/env bash
set -euo pipefail

HOST="${SOLED_HOST:-blog.seudominio.com}"
PORT="${SOLED_PORT:-1915}"
TOKEN_FILE="${SOLED_TOKEN_FILE:-$HOME/.soledade-token}"

if [ "$#" -ne 2 ]; then
  echo "uso: soledade-publish caminho/remoto.md arquivo-local.md" >&2
  exit 1
fi

REMOTE_PATH="$1"
LOCAL_FILE="$2"
TOKEN="$(cat "$TOKEN_FILE")"

{
  printf '%s\n' "$REMOTE_PATH"
  printf '%s\n' "$TOKEN"
  cat "$LOCAL_FILE"
  printf '\n.\n'
} | nc "$HOST" "$PORT"

Instale:

chmod +x soledade-publish
sudo mv soledade-publish /usr/local/bin/

Uso:

soledade-publish posts/noite.md noite.md

22. Melhorando o conteúdo do blog

Modelo de post

# Título do post

Publicado em: 2026-05-31

Primeiro parágrafo direto. Sem enrolação.

## Ideia

Explique a ideia central.

## Desenvolvimento

Desenvolva sem transformar o texto em tese.

## Fechamento

Termine com algo que mereça ficar.

Modelo de página inicial

# Soledade

Uma casa textual em um canto silencioso da rede.

## Entradas

- [Sobre](/about/)
- [Posts](/posts/)
- [Notas](/notes/)
- [Links](/links/)

## Sinal atual

Construindo sistemas pequenos contra a obesidade da web.

Modelo de links

# Links

## Internet textual

- [Nightfall City](https://nightfall.city/)
- [Gemini Protocol](https://geminiprotocol.net/)
- [Project Gemini](https://gemini.circumlunar.space/)

## Ferramentas

- [Python](https://www.python.org/)
- [nginx](https://nginx.org/)
- [Caddy](https://caddyserver.com/)
- [Zola](https://www.getzola.org/)

23. Segurança: onde você pode fazer besteira

Aqui vai a parte sem romantismo.

Nunca aceite caminho livre sem validação

Errado:

path = CONTENT_DIR / user_input

Isso permite ataques tipo:

../../etc/passwd

Certo:

  • remover / inicial;
  • bloquear ..;
  • resolver caminho absoluto;
  • conferir se ele continua dentro de CONTENT_DIR.

O código do tutorial já faz isso.

Não use senha fraca

Use token grande:

openssl rand -hex 32

Não exponha o token no Git

Nunca suba:

secrets/token.txt

Use .gitignore:

secrets/
logs/
venv/
public/

Limite tamanho de postagem

O tutorial usa:

MAX_POST_BYTES = 128 * 1024

Isso evita abuso básico.

Não permita HTML irrestrito se outras pessoas forem postar

Para blog pessoal, ok. Para multiusuário, você precisa sanitizar HTML.

Ferramentas:

Não faça multiusuário agora

Um blog pessoal é fácil. Uma cidade pública é outro jogo.

Multiusuário exige:

  • cadastro;
  • tokens por usuário;
  • quotas;
  • moderação;
  • logs;
  • recuperação de chave;
  • proteção contra spam;
  • backup individual;
  • política de abuso.

Não comece por aí.


24. Backups

Backup simples com tar:

sudo tar -czf /root/soledade-backup-$(date +%F).tar.gz /opt/soledade/content /opt/soledade/secrets

Backup com rsync para outro servidor:

rsync -avz /opt/soledade/content usuario@backup:/backups/soledade/content

Cron diário:

sudo crontab -e

Adicione:

30 3 * * * tar -czf /root/soledade-backup-$(date +\%F).tar.gz /opt/soledade/content /opt/soledade/secrets

25. Logs úteis

Ver serviços:

systemctl status soledade-nex
systemctl status soledade-post

Ver logs em tempo real:

journalctl -u soledade-nex -f
journalctl -u soledade-post -f

Log próprio do publicador:

cat /opt/soledade/logs/post_server.log

Logs nginx:

sudo tail -f /var/log/nginx/soledade.access.log
sudo tail -f /var/log/nginx/soledade.error.log

26. Ideias para evolução

RSS

Gerar /feed.xml para leitores RSS.

Referência:

Sitemap

Gerar /sitemap.xml.

Referência:

Modo Gemini

Criar uma versão .gmi dos posts.

Links:

Busca local

Gerar índice JSON:

/public/search.json

E uma busca simples em JavaScript.

Tags

Usar metadados no topo dos posts:

---
title: Meu post
date: 2026-05-31
tags: [internet, texto, sistemas]
---

Para isso, use:

Editor TUI

Você poderia criar um cliente de terminal com:

Mas não faça isso no começo. Primeiro deixe o sistema simples funcionar.


27. Versão mínima extrema

Se você quiser algo ainda mais bruto, pode ignorar Markdown e servir .txt puro.

Estrutura:

/opt/soledade/content/index.txt
/opt/soledade/content/posts/primeiro.txt

nginx:

server {
    listen 80;
    server_name blog.seudominio.com;
    root /opt/soledade/content;
    autoindex on;
}

Isso é funcional, mas feio para navegador comum. O modelo com Markdown é melhor.


28. Checklist final

Servidor

  • VPS criada
  • Debian/Ubuntu instalado
  • domínio apontando para o IP
  • usuário soledade criado
  • diretórios criados
  • virtualenv criado
  • Python-Markdown instalado

Site

  • config.py ajustado
  • base.html criado
  • index.md criado
  • about.md criado
  • build.py funcionando
  • nginx ou Caddy servindo /opt/soledade/public
  • HTTPS funcionando

Protocolos

  • nex_server.py rodando
  • post_server.py rodando
  • systemd ativado
  • porta 1900 aberta
  • porta 1915 aberta ou restrita ao seu IP

Segurança

  • token forte criado
  • token fora do Git
  • UFW ativo
  • backup configurado
  • logs verificados

29. Comandos rápidos de administração

Reconstruir site:

sudo -u soledade bash -lc 'cd /opt/soledade/soledade && ../venv/bin/python build.py'

Reiniciar serviços:

sudo systemctl restart soledade-nex
sudo systemctl restart soledade-post
sudo systemctl reload nginx

Ver portas:

sudo ss -tulpn | grep -E ':(80|443|1900|1915)'

Testar leitura:

printf "index.md\n" | nc localhost 1900

Testar publicação:

TOKEN=$(sudo cat /opt/soledade/secrets/token.txt)
cat <<EOF | nc localhost 1915
posts/teste-final.md
$TOKEN
# Teste final

Se isto apareceu, o Soledade vive.
.
EOF

30. Minha recomendação final

Construa o Soledade em fases:

Fase 1

Blog estático com Markdown + nginx.

Fase 2

Servidor de leitura textual na porta 1900.

Fase 3

Servidor de postagem na porta 1915.

Fase 4

Cliente PowerShell/Bash para publicar com conforto.

Fase 5

RSS, tags, busca, tema visual e melhorias.

Não tente começar pela versão multiusuário. Isso é a armadilha. Primeiro faça um blog pessoal excelente, pequeno e vivo.

A filosofia correta aqui é:

pequeno > grande
texto > interface inchada
arquivo > banco desnecessário
terminal > painel pesado
clareza > framework da moda

O Soledade deve ser uma ferramenta, não uma plataforma. Plataforma demais vira cemitério de manutenção.


31. Referências

Related documents

Soledade · Murad Library