ADD UPDATE SCRIPT v0.23

This commit is contained in:
Esteban
2026-07-06 12:17:38 +02:00
parent 1d134c38cd
commit ea284a426f
18 changed files with 446 additions and 16 deletions
+27
View File
@@ -88,6 +88,33 @@ publiques se referment automatiquement après.
Relancez simplement `bash scriptsite.sh` : il récupère les derniers changements et reconstruit Relancez simplement `bash scriptsite.sh` : il récupère les derniers changements et reconstruit
la stack sans toucher à vos données. la stack sans toucher à vos données.
Avant chaque redéploiement (s'il existe déjà une instance en cours), le script sauvegarde
automatiquement la base de données dans `backups/bloomfeed-<date>.sql.gz` (les 10 dernières
sauvegardes sont conservées). Si la sauvegarde échoue, la mise à jour est annulée pour ne
jamais risquer de perdre des données. Pour restaurer une sauvegarde :
```bash
gunzip -c backups/bloomfeed-20260706-120000.sql.gz \
| sudo docker compose exec -T db sh -c 'exec mariadb -u root -p"$MARIADB_ROOT_PASSWORD"'
```
### Mise à jour automatique
`auto-update.sh` vérifie périodiquement si le dépôt a de nouveaux commits sur `origin/main` et
ne redéploie (via `scriptsite.sh`) que si c'est le cas — une instance à jour ne reconstruit jamais
pour rien. Le mécanisme tourne sur l'hôte via une tâche cron dédiée (`/etc/cron.d/bloomfeed-auto-update`),
jamais depuis le conteneur applicatif (qui n'a pas accès au démon Docker, par sécurité).
```bash
bash auto-update.sh --install # active la vérification toutes les 30 min (défaut)
bash auto-update.sh --install 60 # ou toutes les 60 min
bash auto-update.sh --status # état actuel + derniers logs
bash auto-update.sh --force # force un redéploiement immédiat
bash auto-update.sh --uninstall # désactive la mise à jour automatique
```
Les logs sont écrits dans `storage/logs/auto-update.log` à l'intérieur du dépôt.
### Réinitialisation ### Réinitialisation
Pour repartir d'une instance vierge (efface comptes, flux, articles et notifieurs) sans toucher Pour repartir d'une instance vierge (efface comptes, flux, articles et notifieurs) sans toucher
+49
View File
@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
use App\Models\User; use App\Models\User;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Response;
use Illuminate\View\View; use Illuminate\View\View;
class PageController extends Controller class PageController extends Controller
@@ -23,4 +24,52 @@ class PageController extends Controller
{ {
return view('self-hosting'); return view('self-hosting');
} }
/**
* Allow/Disallow use paths, not absolute URLs, per the robots.txt spec.
* Only the Sitemap directive takes an absolute URL, generated from this
* instance's own domain so every self-hosted BloomFeed gets it for free.
*/
public function robots(): Response
{
$lines = [
'User-agent: *',
'Allow: /',
'Allow: /self-hosting',
'Disallow: /login',
'Disallow: /register',
'Disallow: /dashboard',
'Disallow: /feeds',
'Disallow: /catalog',
'Disallow: /notifiers',
'Disallow: /settings',
'Disallow: /articles',
'',
'Sitemap: '.route('sitemap'),
];
return response(implode("\n", $lines), 200)->header('Content-Type', 'text/plain');
}
public function sitemap(): Response
{
$urls = [
['loc' => route('home'), 'priority' => '1.0'],
['loc' => route('self-hosting'), 'priority' => '0.8'],
];
$xml = '<?xml version="1.0" encoding="UTF-8"?>'."\n";
$xml .= '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'."\n";
foreach ($urls as $url) {
$xml .= " <url>\n";
$xml .= ' <loc>'.e($url['loc'])."</loc>\n";
$xml .= ' <priority>'.$url['priority']."</priority>\n";
$xml .= " </url>\n";
}
$xml .= '</urlset>';
return response($xml, 200)->header('Content-Type', 'application/xml');
}
} }
Executable
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env bash
set -euo pipefail
# Checks whether the BloomFeed git repository has new commits on origin/main
# and, if so, redeploys via scriptsite.sh. Designed to be run periodically by
# a cron job installed with --install (see below). Only redeploys when there
# is actually something new, so an idle instance never rebuilds for nothing.
APP_DIR="/opt/bloomfeed"
CRON_FILE="/etc/cron.d/bloomfeed-auto-update"
LOG_DIR="${APP_DIR}/storage/logs"
LOG_FILE="${LOG_DIR}/auto-update.log"
INTERVAL_MINUTES=30
FORCE=false
log() {
echo "$(date -Iseconds) $1"
}
case "${1:-}" in
--install)
INTERVAL_MINUTES="${2:-30}"
if ! [[ "${INTERVAL_MINUTES}" =~ ^[0-9]+$ ]] || [[ "${INTERVAL_MINUTES}" -lt 1 || "${INTERVAL_MINUTES}" -gt 1440 ]]; then
echo "ERROR: interval must be a number of minutes between 1 and 1440." >&2
exit 1
fi
sudo mkdir -p "${LOG_DIR}"
echo "*/${INTERVAL_MINUTES} * * * * root ${APP_DIR}/auto-update.sh >> ${LOG_FILE} 2>&1" | sudo tee "${CRON_FILE}" >/dev/null
sudo chmod 644 "${CRON_FILE}"
echo "==> Auto-update installed: checking for new commits every ${INTERVAL_MINUTES} minute(s)."
echo " Cron file: ${CRON_FILE}"
echo " Logs: ${LOG_FILE}"
exit 0
;;
--uninstall)
sudo rm -f "${CRON_FILE}"
echo "==> Auto-update disabled (cron file removed)."
exit 0
;;
--status)
if [[ -f "${CRON_FILE}" ]]; then
echo "==> Auto-update is ENABLED:"
cat "${CRON_FILE}"
else
echo "==> Auto-update is DISABLED (no ${CRON_FILE})."
fi
if [[ -f "${LOG_FILE}" ]]; then
echo "==> Last log entries:"
tail -n 10 "${LOG_FILE}"
fi
exit 0
;;
--force)
FORCE=true
;;
--help|-h)
echo "Usage: bash auto-update.sh [--install [minutes]|--uninstall|--status|--force]"
echo " (no args) Check for new commits and redeploy if found — meant to be run by cron."
echo " --install [N] Install a cron job checking every N minutes (default: 30)."
echo " --uninstall Remove the cron job."
echo " --status Show whether auto-update is enabled and recent log entries."
echo " --force Redeploy immediately, even without a new commit."
exit 0
;;
"") ;;
*)
echo "Unknown option: ${1} (use --help)" >&2
exit 1
;;
esac
if [[ ! -d "${APP_DIR}/.git" ]]; then
echo "ERROR: ${APP_DIR} is not a git repository — run scriptsite.sh first." >&2
exit 1
fi
mkdir -p "${LOG_DIR}"
cd "${APP_DIR}"
git fetch origin main --quiet
LOCAL_SHA="$(git rev-parse HEAD)"
REMOTE_SHA="$(git rev-parse origin/main)"
if [[ "${FORCE}" != true && "${LOCAL_SHA}" == "${REMOTE_SHA}" ]]; then
log "No update available (up to date at ${LOCAL_SHA:0:7})."
exit 0
fi
if [[ "${FORCE}" == true ]]; then
log "Forced redeploy requested (currently at ${LOCAL_SHA:0:7})."
else
log "Update found: ${LOCAL_SHA:0:7} -> ${REMOTE_SHA:0:7}. Redeploying..."
fi
if bash "${APP_DIR}/scriptsite.sh"; then
log "Auto-update finished successfully (now at $(git rev-parse HEAD | cut -c1-7))."
else
log "Auto-update FAILED — the instance may be left in a partially deployed state, check the output above."
exit 1
fi
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
# Forces an immediate redeploy of BloomFeed, regardless of whether a new
# commit is actually available. Thin wrapper around auto-update.sh --force
# so the intent is explicit and easy to remember/document.
APP_DIR="/opt/bloomfeed"
if [[ ! -f "${APP_DIR}/auto-update.sh" ]]; then
echo "ERROR: ${APP_DIR}/auto-update.sh not found — run scriptsite.sh first." >&2
exit 1
fi
echo "==> Forcing an immediate BloomFeed update/redeploy..."
exec bash "${APP_DIR}/auto-update.sh" --force
+13
View File
@@ -8,6 +8,7 @@ return [
'settings' => 'Einstellungen', 'settings' => 'Einstellungen',
'logout' => 'Abmelden', 'logout' => 'Abmelden',
'change_theme' => 'Design wechseln', 'change_theme' => 'Design wechseln',
'nav_menu' => 'Menü',
'language' => 'Sprache', 'language' => 'Sprache',
'self_hosting' => 'Self-Hosting', 'self_hosting' => 'Self-Hosting',
'login' => 'Anmelden', 'login' => 'Anmelden',
@@ -140,6 +141,7 @@ return [
'config_catalog' => 'Der integrierte Feed-Katalog lässt sich durch Bearbeiten von resources/data/bloomflux.json und erneutes Deployment anpassen.', 'config_catalog' => 'Der integrierte Feed-Katalog lässt sich durch Bearbeiten von resources/data/bloomflux.json und erneutes Deployment anpassen.',
'update_title' => 'Aktualisieren', 'update_title' => 'Aktualisieren',
'update_text' => 'Führe einfach das Deployment-Skript erneut aus: Es holt die neuesten Änderungen und baut den Stack neu, ohne deine Daten anzutasten.', 'update_text' => 'Führe einfach das Deployment-Skript erneut aus: Es holt die neuesten Änderungen und baut den Stack neu, ohne deine Daten anzutasten.',
'backup_note' => 'Vor jedem Redeploy wird die Datenbank automatisch nach backups/ gesichert (die letzten 10 Sicherungen werden aufbewahrt). Schlägt die Sicherung fehl, wird das Update abgebrochen, damit deine Daten niemals gefährdet werden.',
'uninstall_title' => 'Deinstallieren', 'uninstall_title' => 'Deinstallieren',
'uninstall_text' => 'Das Skript uninstall.sh stoppt und entfernt den Docker-Stack sauber. Es fragt vor dem Löschen deiner Daten explizit nach und entfernt den Quellcode nie ohne separate Bestätigung.', 'uninstall_text' => 'Das Skript uninstall.sh stoppt und entfernt den Docker-Stack sauber. Es fragt vor dem Löschen deiner Daten explizit nach und entfernt den Quellcode nie ohne separate Bestätigung.',
'source_title' => 'Quellcode', 'source_title' => 'Quellcode',
@@ -191,4 +193,15 @@ return [
'reset_title' => 'Zurücksetzen', 'reset_title' => 'Zurücksetzen',
'reset_text' => 'Das Skript reset.sh setzt die Instanz auf einen leeren Zustand zurück (Konten, Feeds, Artikel, Benachrichtigungen), ohne Quellcode, .env oder Docker-Images anzutasten — nützlich, um mit einer sauberen Instanz neu zu starten.', 'reset_text' => 'Das Skript reset.sh setzt die Instanz auf einen leeren Zustand zurück (Konten, Feeds, Artikel, Benachrichtigungen), ohne Quellcode, .env oder Docker-Images anzutasten — nützlich, um mit einer sauberen Instanz neu zu starten.',
// Automatische Updates
'autoupdate_title' => 'Automatische Updates',
'autoupdate_text' => 'Das Skript auto-update.sh prüft regelmäßig auf neue Commits im Repository und stellt nur bei Bedarf neu bereit, über einen geplanten Task auf dem Server (niemals aus dem Anwendungscontainer heraus).',
'autoupdate_comment_install' => 'prüft alle 30 Minuten',
'autoupdate_comment_status' => 'zeigt Status und letzte Logs',
'autoupdate_comment_uninstall' => 'deaktiviert automatische Updates',
// SEO
'seo_home_title' => 'BloomFeed — Selbst gehosteter, quelloffener RSS-Aggregator',
'seo_selfhost_title' => 'BloomFeed selbst hosten — Docker-Installationsanleitung',
]; ];
+13
View File
@@ -8,6 +8,7 @@ return [
'settings' => 'Settings', 'settings' => 'Settings',
'logout' => 'Log out', 'logout' => 'Log out',
'change_theme' => 'Switch theme', 'change_theme' => 'Switch theme',
'nav_menu' => 'Menu',
'language' => 'Language', 'language' => 'Language',
'self_hosting' => 'Self-hosting', 'self_hosting' => 'Self-hosting',
'login' => 'Log in', 'login' => 'Log in',
@@ -140,6 +141,7 @@ return [
'config_catalog' => 'The built-in feed catalog can be customized by editing resources/data/bloomflux.json and redeploying.', 'config_catalog' => 'The built-in feed catalog can be customized by editing resources/data/bloomflux.json and redeploying.',
'update_title' => 'Update', 'update_title' => 'Update',
'update_text' => 'Simply re-run the deployment script: it pulls the latest changes and rebuilds the stack without touching your data.', 'update_text' => 'Simply re-run the deployment script: it pulls the latest changes and rebuilds the stack without touching your data.',
'backup_note' => 'Before every redeploy, the database is automatically backed up to backups/ (the last 10 backups are kept). If the backup fails, the update is aborted so your data is never put at risk.',
'uninstall_title' => 'Uninstall', 'uninstall_title' => 'Uninstall',
'uninstall_text' => 'The uninstall.sh script cleanly stops and removes the Docker stack. It asks for explicit confirmation before deleting your data and never removes the source code without a separate confirmation.', 'uninstall_text' => 'The uninstall.sh script cleanly stops and removes the Docker stack. It asks for explicit confirmation before deleting your data and never removes the source code without a separate confirmation.',
'source_title' => 'Source code', 'source_title' => 'Source code',
@@ -191,4 +193,15 @@ return [
'reset_title' => 'Reset', 'reset_title' => 'Reset',
'reset_text' => 'The reset.sh script wipes the instance back to a blank state (accounts, feeds, articles, notifiers) without touching the source code, .env or Docker images — useful to start over on a clean instance.', 'reset_text' => 'The reset.sh script wipes the instance back to a blank state (accounts, feeds, articles, notifiers) without touching the source code, .env or Docker images — useful to start over on a clean instance.',
// Auto-update
'autoupdate_title' => 'Automatic updates',
'autoupdate_text' => 'The auto-update.sh script periodically checks for new commits on the repository and only redeploys when needed, via a scheduled task on the server (never from the application container).',
'autoupdate_comment_install' => 'checks every 30 minutes',
'autoupdate_comment_status' => 'shows status and recent logs',
'autoupdate_comment_uninstall' => 'disables automatic updates',
// SEO
'seo_home_title' => 'BloomFeed — Self-hosted, open-source RSS aggregator',
'seo_selfhost_title' => 'Self-host BloomFeed — Docker installation guide',
]; ];
+13
View File
@@ -8,6 +8,7 @@ return [
'settings' => 'Ajustes', 'settings' => 'Ajustes',
'logout' => 'Cerrar sesión', 'logout' => 'Cerrar sesión',
'change_theme' => 'Cambiar de tema', 'change_theme' => 'Cambiar de tema',
'nav_menu' => 'Menú',
'language' => 'Idioma', 'language' => 'Idioma',
'self_hosting' => 'Autoalojamiento', 'self_hosting' => 'Autoalojamiento',
'login' => 'Iniciar sesión', 'login' => 'Iniciar sesión',
@@ -140,6 +141,7 @@ return [
'config_catalog' => 'El catálogo de fuentes integrado se personaliza editando resources/data/bloomflux.json y volviendo a desplegar.', 'config_catalog' => 'El catálogo de fuentes integrado se personaliza editando resources/data/bloomflux.json y volviendo a desplegar.',
'update_title' => 'Actualizar', 'update_title' => 'Actualizar',
'update_text' => 'Simplemente vuelve a ejecutar el script de despliegue: recupera los últimos cambios y reconstruye la pila sin tocar tus datos.', 'update_text' => 'Simplemente vuelve a ejecutar el script de despliegue: recupera los últimos cambios y reconstruye la pila sin tocar tus datos.',
'backup_note' => 'Antes de cada redespliegue, la base de datos se respalda automáticamente en backups/ (se conservan las últimas 10 copias). Si el respaldo falla, la actualización se cancela para no arriesgar nunca tus datos.',
'uninstall_title' => 'Desinstalar', 'uninstall_title' => 'Desinstalar',
'uninstall_text' => 'El script uninstall.sh detiene y elimina limpiamente la pila Docker. Pide confirmación explícita antes de borrar tus datos y nunca elimina el código fuente sin una confirmación separada.', 'uninstall_text' => 'El script uninstall.sh detiene y elimina limpiamente la pila Docker. Pide confirmación explícita antes de borrar tus datos y nunca elimina el código fuente sin una confirmación separada.',
'source_title' => 'Código fuente', 'source_title' => 'Código fuente',
@@ -191,4 +193,15 @@ return [
'reset_title' => 'Reiniciar', 'reset_title' => 'Reiniciar',
'reset_text' => 'El script reset.sh restablece la instancia a un estado en blanco (cuentas, fuentes, artículos, notificadores) sin tocar el código fuente, el .env ni las imágenes Docker — útil para empezar de cero.', 'reset_text' => 'El script reset.sh restablece la instancia a un estado en blanco (cuentas, fuentes, artículos, notificadores) sin tocar el código fuente, el .env ni las imágenes Docker — útil para empezar de cero.',
// Actualización automática
'autoupdate_title' => 'Actualizaciones automáticas',
'autoupdate_text' => 'El script auto-update.sh comprueba periódicamente si hay nuevos commits en el repositorio y solo vuelve a desplegar cuando es necesario, mediante una tarea programada en el servidor (nunca desde el contenedor de la aplicación).',
'autoupdate_comment_install' => 'comprueba cada 30 minutos',
'autoupdate_comment_status' => 'muestra el estado y los últimos registros',
'autoupdate_comment_uninstall' => 'desactiva las actualizaciones automáticas',
// SEO
'seo_home_title' => 'BloomFeed — Agregador RSS autoalojado y de código abierto',
'seo_selfhost_title' => 'Autoalojar BloomFeed — guía de instalación con Docker',
]; ];
+13
View File
@@ -8,6 +8,7 @@ return [
'settings' => 'Paramètres', 'settings' => 'Paramètres',
'logout' => 'Déconnexion', 'logout' => 'Déconnexion',
'change_theme' => 'Changer de thème', 'change_theme' => 'Changer de thème',
'nav_menu' => 'Menu',
'language' => 'Langue', 'language' => 'Langue',
'self_hosting' => 'Self-hosting', 'self_hosting' => 'Self-hosting',
'login' => 'Se connecter', 'login' => 'Se connecter',
@@ -140,6 +141,7 @@ return [
'config_catalog' => "Le catalogue de flux intégré se personnalise en éditant resources/data/bloomflux.json puis en redéployant.", 'config_catalog' => "Le catalogue de flux intégré se personnalise en éditant resources/data/bloomflux.json puis en redéployant.",
'update_title' => 'Mettre à jour', 'update_title' => 'Mettre à jour',
'update_text' => 'Relancez simplement le script de déploiement : il récupère les derniers changements et reconstruit la stack sans toucher à vos données.', 'update_text' => 'Relancez simplement le script de déploiement : il récupère les derniers changements et reconstruit la stack sans toucher à vos données.',
'backup_note' => 'Avant chaque redéploiement, la base de données est automatiquement sauvegardée dans backups/ (les 10 dernières sauvegardes sont conservées). Si la sauvegarde échoue, la mise à jour est annulée pour ne jamais risquer de perdre des données.',
'uninstall_title' => 'Désinstaller', 'uninstall_title' => 'Désinstaller',
'uninstall_text' => "Le script uninstall.sh arrête et supprime proprement la stack Docker. Il demande une confirmation explicite avant de supprimer vos données et ne supprime jamais le code source sans confirmation séparée.", 'uninstall_text' => "Le script uninstall.sh arrête et supprime proprement la stack Docker. Il demande une confirmation explicite avant de supprimer vos données et ne supprime jamais le code source sans confirmation séparée.",
'source_title' => 'Code source', 'source_title' => 'Code source',
@@ -191,4 +193,15 @@ return [
'reset_title' => 'Réinitialiser', 'reset_title' => 'Réinitialiser',
'reset_text' => "Le script reset.sh remet l'instance à zéro (comptes, flux, articles, notifieurs) sans toucher au code, au .env ni aux images Docker — utile pour repartir sur une instance vierge.", 'reset_text' => "Le script reset.sh remet l'instance à zéro (comptes, flux, articles, notifieurs) sans toucher au code, au .env ni aux images Docker — utile pour repartir sur une instance vierge.",
// Mise à jour automatique
'autoupdate_title' => 'Mise à jour automatique',
'autoupdate_text' => "Le script auto-update.sh vérifie périodiquement les nouveaux commits sur le dépôt et ne redéploie que si nécessaire, via une tâche planifiée sur le serveur (jamais depuis le conteneur applicatif).",
'autoupdate_comment_install' => 'active la vérification toutes les 30 min',
'autoupdate_comment_status' => 'affiche l\'état et les derniers logs',
'autoupdate_comment_uninstall' => 'désactive la mise à jour automatique',
// SEO
'seo_home_title' => 'BloomFeed — Agrégateur RSS auto-hébergé et open-source',
'seo_selfhost_title' => "Auto-héberger BloomFeed — guide d'installation Docker",
]; ];
+74 -9
View File
@@ -224,20 +224,85 @@ h1, h2, h3 {
color: var(--accent-contrast); color: var(--accent-contrast);
} }
@media (max-width: 640px) { .navbar-toggle {
.navbar { display: none;
padding: 12px 16px; flex-direction: column;
justify-content: center;
align-items: center;
gap: 5px;
width: 40px;
height: 40px;
background: transparent;
border: 1.5px solid var(--border);
border-radius: var(--radius-sm);
cursor: pointer;
flex-shrink: 0;
}
.navbar-toggle-bar {
display: block;
width: 18px;
height: 2px;
border-radius: var(--radius-full);
background: var(--text);
transition: transform var(--t-fast), opacity var(--t-fast);
}
.navbar-toggle.is-active .navbar-toggle-bar:nth-child(1) {
transform: translateY(7px) rotate(45deg);
}
.navbar-toggle.is-active .navbar-toggle-bar:nth-child(2) {
opacity: 0;
}
.navbar-toggle.is-active .navbar-toggle-bar:nth-child(3) {
transform: translateY(-7px) rotate(-45deg);
}
@media (max-width: 900px) {
.navbar-toggle {
display: flex;
} }
.navbar-links { .navbar-links {
gap: 8px; display: none;
flex-wrap: wrap; position: absolute;
justify-content: flex-end; top: 100%;
row-gap: 6px; left: 0;
right: 0;
flex-direction: column;
align-items: stretch;
gap: 4px;
padding: var(--space-3) 16px calc(var(--space-4) + 4px);
background: var(--surface);
border-bottom: 1px solid var(--border);
box-shadow: var(--shadow-md);
} }
.nav-self-hosting { .navbar-links.is-open {
display: none; display: flex;
}
.navbar-links a,
.navbar-links form {
width: 100%;
}
.navbar-links a {
padding: 10px 4px;
}
.navbar-links a.btn,
.navbar-links form button {
width: 100%;
text-align: center;
}
.navbar-links .locale-form,
.navbar-links .theme-toggle {
align-self: flex-start;
margin-top: 4px;
} }
} }
+27
View File
@@ -0,0 +1,27 @@
(function () {
document.addEventListener('DOMContentLoaded', function () {
var toggle = document.querySelector('[data-navbar-toggle]');
if (!toggle) {
return;
}
var links = document.getElementById(toggle.getAttribute('aria-controls'));
if (!links) {
return;
}
toggle.addEventListener('click', function () {
var isOpen = links.classList.toggle('is-open');
toggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
toggle.classList.toggle('is-active', isOpen);
});
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape' && links.classList.contains('is-open')) {
links.classList.remove('is-open');
toggle.setAttribute('aria-expanded', 'false');
toggle.classList.remove('is-active');
}
});
});
})();
-2
View File
@@ -1,2 +0,0 @@
User-agent: *
Disallow:
+30 -1
View File
@@ -1,4 +1,7 @@
@extends('layouts.marketing', ['title' => 'BloomFeed']) @extends('layouts.marketing', [
'title' => __('app.seo_home_title'),
'description' => __('app.meta_description'),
])
@php @php
// Screenshots live under images/screenshots/{locale}/ so each language can show its own // Screenshots live under images/screenshots/{locale}/ so each language can show its own
@@ -129,4 +132,30 @@
</section> </section>
<script src="{{ asset('js/screenshot-gallery.js') }}" defer></script> <script src="{{ asset('js/screenshot-gallery.js') }}" defer></script>
@push('structured-data')
<script type="application/ld+json">
{
"@@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "BloomFeed",
"applicationCategory": "CommunicationApplication",
"operatingSystem": "Linux (Docker)",
"description": {!! json_encode(__('app.meta_description')) !!},
"url": {!! json_encode(url('/')) !!},
"image": {!! json_encode(asset('images/logo.png')) !!},
"license": "https://opensource.org/licenses/MIT",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"author": {
"@type": "Organization",
"name": "BloomFeed",
"url": {!! json_encode(url('/self-hosting')) !!}
}
}
</script>
@endpush
@endsection @endsection
+1
View File
@@ -27,6 +27,7 @@
</div> </div>
<script src="{{ asset('js/theme-toggle.js') }}" defer></script> <script src="{{ asset('js/theme-toggle.js') }}" defer></script>
<script src="{{ asset('js/navbar-toggle.js') }}" defer></script>
<script src="{{ asset('js/app.js') }}" defer></script> <script src="{{ asset('js/app.js') }}" defer></script>
</body> </body>
</html> </html>
+26 -2
View File
@@ -5,9 +5,27 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}"> <meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ $title ?? 'BloomFeed' }}</title> <title>{{ $title ?? 'BloomFeed' }}</title>
<meta name="description" content="{{ __('app.meta_description') }}"> <meta name="description" content="{{ $description ?? __('app.meta_description') }}">
<link rel="canonical" href="{{ url()->current() }}">
<link rel="icon" href="{{ asset('images/icon.png') }}"> <link rel="icon" href="{{ asset('images/icon.png') }}">
<link rel="apple-touch-icon" href="{{ asset('images/icon.png') }}"> <link rel="apple-touch-icon" href="{{ asset('images/icon.png') }}">
<meta property="og:type" content="website">
<meta property="og:site_name" content="BloomFeed">
<meta property="og:locale" content="{{ str_replace('-', '_', app()->getLocale()) }}">
<meta property="og:title" content="{{ $title ?? 'BloomFeed' }}">
<meta property="og:description" content="{{ $description ?? __('app.meta_description') }}">
<meta property="og:url" content="{{ url()->current() }}">
<meta property="og:image" content="{{ asset('images/logo.png') }}">
<meta property="og:image:width" content="1917">
<meta property="og:image:height" content="544">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{ $title ?? 'BloomFeed' }}">
<meta name="twitter:description" content="{{ $description ?? __('app.meta_description') }}">
<meta name="twitter:image" content="{{ asset('images/logo.png') }}">
@stack('structured-data')
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css">
@@ -27,7 +45,12 @@
<img src="{{ asset('images/logo.png') }}" alt="BloomFeed" class="brand-logo brand-logo-light"> <img src="{{ asset('images/logo.png') }}" alt="BloomFeed" class="brand-logo brand-logo-light">
<img src="{{ asset('images/logo-dark.png') }}" alt="BloomFeed" class="brand-logo brand-logo-dark"> <img src="{{ asset('images/logo-dark.png') }}" alt="BloomFeed" class="brand-logo brand-logo-dark">
</a> </a>
<div class="navbar-links"> <button type="button" class="navbar-toggle" data-navbar-toggle aria-expanded="false" aria-controls="navbarLinks" title="{{ __('app.nav_menu') }}">
<span class="navbar-toggle-bar"></span>
<span class="navbar-toggle-bar"></span>
<span class="navbar-toggle-bar"></span>
</button>
<div class="navbar-links" id="navbarLinks">
<a href="{{ route('self-hosting') }}" class="nav-self-hosting {{ request()->routeIs('self-hosting') ? 'active' : '' }}">{{ __('app.self_hosting') }}</a> <a href="{{ route('self-hosting') }}" class="nav-self-hosting {{ request()->routeIs('self-hosting') ? 'active' : '' }}">{{ __('app.self_hosting') }}</a>
@include('partials.locale-selector') @include('partials.locale-selector')
<button type="button" class="theme-toggle" data-theme-toggle title="{{ __('app.change_theme') }}"></button> <button type="button" class="theme-toggle" data-theme-toggle title="{{ __('app.change_theme') }}"></button>
@@ -49,6 +72,7 @@
</footer> </footer>
<script src="{{ asset('js/theme-toggle.js') }}" defer></script> <script src="{{ asset('js/theme-toggle.js') }}" defer></script>
<script src="{{ asset('js/navbar-toggle.js') }}" defer></script>
<script src="{{ asset('js/scroll-reveal.js') }}" defer></script> <script src="{{ asset('js/scroll-reveal.js') }}" defer></script>
</body> </body>
</html> </html>
+6 -1
View File
@@ -3,7 +3,12 @@
<img src="{{ asset('images/logo.png') }}" alt="BloomFeed" class="brand-logo brand-logo-light"> <img src="{{ asset('images/logo.png') }}" alt="BloomFeed" class="brand-logo brand-logo-light">
<img src="{{ asset('images/logo-dark.png') }}" alt="BloomFeed" class="brand-logo brand-logo-dark"> <img src="{{ asset('images/logo-dark.png') }}" alt="BloomFeed" class="brand-logo brand-logo-dark">
</a> </a>
<div class="navbar-links"> <button type="button" class="navbar-toggle" data-navbar-toggle aria-expanded="false" aria-controls="navbarLinks" title="{{ __('app.nav_menu') }}">
<span class="navbar-toggle-bar"></span>
<span class="navbar-toggle-bar"></span>
<span class="navbar-toggle-bar"></span>
</button>
<div class="navbar-links" id="navbarLinks">
<a href="{{ route('dashboard') }}" class="{{ request()->routeIs('dashboard') ? 'active' : '' }}">{{ __('app.articles') }}</a> <a href="{{ route('dashboard') }}" class="{{ request()->routeIs('dashboard') ? 'active' : '' }}">{{ __('app.articles') }}</a>
<a href="{{ route('catalog') }}" class="{{ request()->routeIs('catalog') ? 'active' : '' }}">{{ __('app.catalog') }}</a> <a href="{{ route('catalog') }}" class="{{ request()->routeIs('catalog') ? 'active' : '' }}">{{ __('app.catalog') }}</a>
<a href="{{ route('feeds.index') }}" class="{{ request()->routeIs('feeds.*') ? 'active' : '' }}">{{ __('app.my_feeds') }}</a> <a href="{{ route('feeds.index') }}" class="{{ request()->routeIs('feeds.*') ? 'active' : '' }}">{{ __('app.my_feeds') }}</a>
+13 -1
View File
@@ -1,4 +1,7 @@
@extends('layouts.marketing', ['title' => __('app.selfhost_title').' — BloomFeed']) @extends('layouts.marketing', [
'title' => __('app.seo_selfhost_title'),
'description' => __('app.selfhost_intro'),
])
@section('content') @section('content')
<section class="docs-shell"> <section class="docs-shell">
@@ -45,6 +48,15 @@ cd bloomfeed</pre>
<h2>{{ __('app.update_title') }}</h2> <h2>{{ __('app.update_title') }}</h2>
<p>{{ __('app.update_text') }}</p> <p>{{ __('app.update_text') }}</p>
<pre class="code-block">bash scriptsite.sh</pre> <pre class="code-block">bash scriptsite.sh</pre>
<p>{{ __('app.backup_note') }}</p>
</div>
<div class="card mt-2">
<h2>{{ __('app.autoupdate_title') }}</h2>
<p>{{ __('app.autoupdate_text') }}</p>
<pre class="code-block">bash auto-update.sh --install # {{ __('app.autoupdate_comment_install') }}
bash auto-update.sh --status # {{ __('app.autoupdate_comment_status') }}
bash auto-update.sh --uninstall # {{ __('app.autoupdate_comment_uninstall') }}</pre>
</div> </div>
<div class="card mt-2"> <div class="card mt-2">
+2
View File
@@ -12,6 +12,8 @@ use Illuminate\Support\Facades\Route;
Route::get('/', [PageController::class, 'home'])->name('home'); Route::get('/', [PageController::class, 'home'])->name('home');
Route::get('/self-hosting', [PageController::class, 'selfHosting'])->name('self-hosting'); Route::get('/self-hosting', [PageController::class, 'selfHosting'])->name('self-hosting');
Route::get('/robots.txt', [PageController::class, 'robots'])->name('robots');
Route::get('/sitemap.xml', [PageController::class, 'sitemap'])->name('sitemap');
Route::post('/locale', [SettingsController::class, 'updateLocale'])->name('locale.update'); Route::post('/locale', [SettingsController::class, 'updateLocale'])->name('locale.update');
Route::middleware('guest')->group(function () { Route::middleware('guest')->group(function () {
+22
View File
@@ -4,6 +4,7 @@ set -euo pipefail
REPO_URL="https://git.eldorianet.work/esteban/BloomFeed-RSS" REPO_URL="https://git.eldorianet.work/esteban/BloomFeed-RSS"
APP_DIR="/opt/bloomfeed" APP_DIR="/opt/bloomfeed"
HOST_PORT="7081" HOST_PORT="7081"
BACKUP_RETENTION="10"
echo "==> Killing any process listening on port ${HOST_PORT}" echo "==> Killing any process listening on port ${HOST_PORT}"
PIDS="$(sudo lsof -ti tcp:${HOST_PORT} || true)" PIDS="$(sudo lsof -ti tcp:${HOST_PORT} || true)"
@@ -48,6 +49,27 @@ if grep -q '^DB_ROOT_PASSWORD=$' .env; then
fi fi
chmod 600 .env chmod 600 .env
echo "==> Backing up the database before redeploying (if a previous instance is running)"
BACKUP_DIR="${APP_DIR}/backups"
if [[ -n "$(sudo docker compose ps -q db 2>/dev/null)" ]]; then
sudo mkdir -p "${BACKUP_DIR}"
BACKUP_FILE="${BACKUP_DIR}/bloomfeed-$(date +%Y%m%d-%H%M%S).sql.gz"
# shellcheck disable=SC2016
if sudo docker compose exec -T db sh -c 'exec mariadb-dump -u root -p"$MARIADB_ROOT_PASSWORD" --all-databases' | gzip > "${BACKUP_FILE}"; then
sudo chmod 600 "${BACKUP_FILE}"
echo "==> Backup saved: ${BACKUP_FILE}"
echo "==> Pruning old backups (keeping the last ${BACKUP_RETENTION})"
(sudo find "${BACKUP_DIR}" -maxdepth 1 -name 'bloomfeed-*.sql.gz' -print | sort | head -n -"${BACKUP_RETENTION}" | xargs -r sudo rm -f) || echo "WARNING: pruning old backups failed (non-fatal, continuing) — check ${BACKUP_DIR} manually." >&2
else
sudo rm -f "${BACKUP_FILE}"
echo "ERROR: database backup failed — aborting the update so no data is put at risk." >&2
echo " Check 'sudo docker compose logs db' and retry manually." >&2
exit 1
fi
else
echo "==> No running database container found (fresh install) — skipping backup"
fi
echo "==> Stopping the previous Docker Compose stack if present" echo "==> Stopping the previous Docker Compose stack if present"
sudo docker compose down --remove-orphans || true sudo docker compose down --remove-orphans || true