ADD UPDATE SCRIPT v0.23
This commit is contained in:
@@ -88,6 +88,33 @@ publiques se referment automatiquement après.
|
||||
Relancez simplement `bash scriptsite.sh` : il récupère les derniers changements et reconstruit
|
||||
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
|
||||
|
||||
Pour repartir d'une instance vierge (efface comptes, flux, articles et notifieurs) sans toucher
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class PageController extends Controller
|
||||
@@ -23,4 +24,52 @@ class PageController extends Controller
|
||||
{
|
||||
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
@@ -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
|
||||
Executable
+16
@@ -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
|
||||
@@ -8,6 +8,7 @@ return [
|
||||
'settings' => 'Einstellungen',
|
||||
'logout' => 'Abmelden',
|
||||
'change_theme' => 'Design wechseln',
|
||||
'nav_menu' => 'Menü',
|
||||
'language' => 'Sprache',
|
||||
'self_hosting' => 'Self-Hosting',
|
||||
'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.',
|
||||
'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.',
|
||||
'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_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',
|
||||
@@ -191,4 +193,15 @@ return [
|
||||
|
||||
'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.',
|
||||
|
||||
// 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',
|
||||
];
|
||||
|
||||
@@ -8,6 +8,7 @@ return [
|
||||
'settings' => 'Settings',
|
||||
'logout' => 'Log out',
|
||||
'change_theme' => 'Switch theme',
|
||||
'nav_menu' => 'Menu',
|
||||
'language' => 'Language',
|
||||
'self_hosting' => 'Self-hosting',
|
||||
'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.',
|
||||
'update_title' => 'Update',
|
||||
'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_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',
|
||||
@@ -191,4 +193,15 @@ return [
|
||||
|
||||
'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.',
|
||||
|
||||
// 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',
|
||||
];
|
||||
|
||||
@@ -8,6 +8,7 @@ return [
|
||||
'settings' => 'Ajustes',
|
||||
'logout' => 'Cerrar sesión',
|
||||
'change_theme' => 'Cambiar de tema',
|
||||
'nav_menu' => 'Menú',
|
||||
'language' => 'Idioma',
|
||||
'self_hosting' => 'Autoalojamiento',
|
||||
'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.',
|
||||
'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.',
|
||||
'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_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',
|
||||
@@ -191,4 +193,15 @@ return [
|
||||
|
||||
'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.',
|
||||
|
||||
// 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',
|
||||
];
|
||||
|
||||
@@ -8,6 +8,7 @@ return [
|
||||
'settings' => 'Paramètres',
|
||||
'logout' => 'Déconnexion',
|
||||
'change_theme' => 'Changer de thème',
|
||||
'nav_menu' => 'Menu',
|
||||
'language' => 'Langue',
|
||||
'self_hosting' => 'Self-hosting',
|
||||
'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.",
|
||||
'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.',
|
||||
'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_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',
|
||||
@@ -191,4 +193,15 @@ return [
|
||||
|
||||
'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.",
|
||||
|
||||
// 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
@@ -224,20 +224,85 @@ h1, h2, h3 {
|
||||
color: var(--accent-contrast);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.navbar {
|
||||
padding: 12px 16px;
|
||||
.navbar-toggle {
|
||||
display: none;
|
||||
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 {
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
row-gap: 6px;
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
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 {
|
||||
display: none;
|
||||
.navbar-links.is-open {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
@@ -1,2 +0,0 @@
|
||||
User-agent: *
|
||||
Disallow:
|
||||
@@ -1,4 +1,7 @@
|
||||
@extends('layouts.marketing', ['title' => 'BloomFeed'])
|
||||
@extends('layouts.marketing', [
|
||||
'title' => __('app.seo_home_title'),
|
||||
'description' => __('app.meta_description'),
|
||||
])
|
||||
|
||||
@php
|
||||
// Screenshots live under images/screenshots/{locale}/ so each language can show its own
|
||||
@@ -129,4 +132,30 @@
|
||||
</section>
|
||||
|
||||
<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
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,9 +5,27 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<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="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 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">
|
||||
@@ -27,7 +45,12 @@
|
||||
<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">
|
||||
</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>
|
||||
@include('partials.locale-selector')
|
||||
<button type="button" class="theme-toggle" data-theme-toggle title="{{ __('app.change_theme') }}">◐</button>
|
||||
@@ -49,6 +72,7 @@
|
||||
</footer>
|
||||
|
||||
<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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
<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">
|
||||
</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('catalog') }}" class="{{ request()->routeIs('catalog') ? 'active' : '' }}">{{ __('app.catalog') }}</a>
|
||||
<a href="{{ route('feeds.index') }}" class="{{ request()->routeIs('feeds.*') ? 'active' : '' }}">{{ __('app.my_feeds') }}</a>
|
||||
|
||||
@@ -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 class="docs-shell">
|
||||
@@ -45,6 +48,15 @@ cd bloomfeed</pre>
|
||||
<h2>{{ __('app.update_title') }}</h2>
|
||||
<p>{{ __('app.update_text') }}</p>
|
||||
<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 class="card mt-2">
|
||||
|
||||
@@ -12,6 +12,8 @@ use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/', [PageController::class, 'home'])->name('home');
|
||||
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::middleware('guest')->group(function () {
|
||||
|
||||
@@ -4,6 +4,7 @@ set -euo pipefail
|
||||
REPO_URL="https://git.eldorianet.work/esteban/BloomFeed-RSS"
|
||||
APP_DIR="/opt/bloomfeed"
|
||||
HOST_PORT="7081"
|
||||
BACKUP_RETENTION="10"
|
||||
|
||||
echo "==> Killing any process listening on port ${HOST_PORT}"
|
||||
PIDS="$(sudo lsof -ti tcp:${HOST_PORT} || true)"
|
||||
@@ -48,6 +49,27 @@ if grep -q '^DB_ROOT_PASSWORD=$' .env; then
|
||||
fi
|
||||
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"
|
||||
sudo docker compose down --remove-orphans || true
|
||||
|
||||
|
||||
Reference in New Issue
Block a user