simpleblog (20006B)
1 #!/bin/sh 2 # simpleblog — generador estatico minimalista de blog 3 # uso: simpleblog [init|build|clean|stats|serve] 4 # 5 # trabaja sobre el directorio actual (el sitio). las plantillas y el skel 6 # por defecto viven en $SIMPLEBLOG_DEFAULTS (default /usr/share/simpleblog). 7 # si el sitio tiene su propia ./templates/<name>, esa gana sobre el default. 8 set -eu 9 10 SITE_DIR="${SIMPLEBLOG_SITE:-$(pwd)}" 11 DEFAULTS="${SIMPLEBLOG_DEFAULTS:-/usr/share/simpleblog}" 12 13 OUT="${SIMPLEBLOG_OUT:-$SITE_DIR/public}" 14 POSTS="$SITE_DIR/posts" 15 PAGES="$SITE_DIR/pages" 16 STATIC="$SITE_DIR/static" 17 IMAGES="$SITE_DIR/images" 18 CONF="$SITE_DIR/config" 19 20 # devuelve la ruta a una plantilla con override: si existe en el sitio, esa; 21 # si no, la del directorio de defaults. 22 tpl() { 23 if [ -f "$SITE_DIR/templates/$1" ]; then 24 printf '%s\n' "$SITE_DIR/templates/$1" 25 else 26 printf '%s\n' "$DEFAULTS/templates/$1" 27 fi 28 } 29 30 # --- utilidades ---------------------------------------------------------- 31 32 # leer valor del fichero de config (key: value) 33 conf() { 34 awk -v k="$1" ' 35 /^[[:space:]]*#/ {next} 36 { 37 i=index($0, ":") 38 if (!i) next 39 key=substr($0, 1, i-1); val=substr($0, i+1) 40 gsub(/^[[:space:]]+|[[:space:]]+$/, "", key) 41 gsub(/^[[:space:]]+|[[:space:]]+$/, "", val) 42 if (key==k) { print val; exit } 43 } 44 ' "$CONF" 45 } 46 47 # escape para insertar valores en plantillas via sed 48 sed_escape() { 49 printf '%s' "$1" | sed 's/[&/\]/\\&/g; s/$/\\n/' | tr -d '\n' | sed 's/\\n$//' 50 } 51 52 # sustituye {{key}} en $1 por valor de $2 (escapado para sed) 53 sub_var() { 54 sed -i "s/{{$1}}/$2/g" "$3" 55 } 56 57 # sustituye {{key}} en $1 por el contenido literal del fichero $2. 58 # usamos awk porque sed se atraganta con & \ y newlines del contenido. 59 sub_file() { 60 key="$1"; file="$2"; target="$3" 61 awk -v key="{{$key}}" -v file="$file" ' 62 BEGIN { 63 while ((getline line < file) > 0) blob = blob line "\n" 64 close(file) 65 sub(/\n$/, "", blob) 66 } 67 { 68 while ((n = index($0, key)) > 0) { 69 printf "%s", substr($0, 1, n-1) 70 printf "%s", blob 71 $0 = substr($0, n + length(key)) 72 } 73 print 74 } 75 ' "$target" > "$target.tmp" && mv "$target.tmp" "$target" 76 } 77 78 # aplica todas las variables de $2 (key<TAB>valor) a la plantilla $1 -> $3 79 render() { 80 template="$1"; vars="$2"; out="$3" 81 cp "$template" "$out" 82 while IFS=' ' read -r k v; do 83 [ -z "$k" ] && continue 84 sub_var "$k" "$v" "$out" 85 done < "$vars" 86 } 87 88 # extraer frontmatter de un .md/.html. cuerpo -> $2, meta key<TAB>valor -> $3 89 parse_post() { 90 awk -v body="$2" -v meta="$3" ' 91 BEGIN { inhead=1 } 92 inhead && /^[[:space:]]*$/ { inhead=0; next } 93 inhead { 94 i=index($0, ":") 95 if (!i) next 96 k=substr($0, 1, i-1); v=substr($0, i+1) 97 gsub(/^[[:space:]]+|[[:space:]]+$/, "", k) 98 gsub(/^[[:space:]]+|[[:space:]]+$/, "", v) 99 print k "\t" v > meta 100 next 101 } 102 { print > body } 103 ' "$1" 104 } 105 106 meta_get() { 107 awk -F'\t' -v k="$1" '$1==k {print $2; exit}' "$2" 108 } 109 110 xml_escape() { 111 printf '%s' "$1" | sed -e 's/&/\&/g' -e 's/</\</g' -e 's/>/\>/g' \ 112 -e "s/'/\'/g" -e 's/"/\"/g' 113 } 114 115 # YYYY-MM-DD -> YYYY-MM-DDT00:00:00Z; el resto lo deja 116 to_rfc3339() { 117 case "$1" in 118 ????-??-??) printf '%sT00:00:00Z' "$1" ;; 119 *) printf '%s' "$1" ;; 120 esac 121 } 122 123 # splittea "foo, bar,baz" -> una tag por linea, trim de espacios 124 split_tags() { 125 printf '%s' "$1" | tr ',' '\n' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' \ 126 | grep -v '^$' || true 127 } 128 129 # true si $1 (origen) no existe o es mas nuevo que $2 (destino) 130 needs_update() { 131 [ -f "$2" ] || return 0 132 [ "$1" -nt "$2" ] && return 0 133 return 1 134 } 135 136 # procesar images/: copiar original y generar variantes por cada ancho. 137 # no hace upscale (el ">" de magick); cachea por mtime. 138 process_images() { 139 [ -d "$IMAGES" ] || return 0 140 141 sizes=$(conf image_sizes) 142 quality=$(conf image_quality) 143 [ -z "$quality" ] && quality=85 144 extras=$(conf image_extra_formats) 145 146 if ! command -v magick >/dev/null 2>&1 && ! command -v convert >/dev/null 2>&1; then 147 echo "warn: ni magick ni convert disponibles, copiando images/ tal cual" >&2 148 mkdir -p "$OUT/img" 149 cp -r "$IMAGES"/. "$OUT/img"/ 150 return 0 151 fi 152 if command -v magick >/dev/null 2>&1; then 153 IM="magick" 154 else 155 IM="convert" 156 fi 157 158 mkdir -p "$OUT/img" 159 gen=0; cached=0 160 for img in "$IMAGES"/*; do 161 [ -f "$img" ] || continue 162 name=${img##*/} 163 base=${name%.*} 164 ext=${name##*.} 165 166 # original (sin sufijo) 167 if needs_update "$img" "$OUT/img/$name"; then 168 cp "$img" "$OUT/img/$name" 169 gen=$((gen+1)) 170 else 171 cached=$((cached+1)) 172 fi 173 174 [ -z "$sizes" ] && continue 175 for w in $(printf '%s' "$sizes" | tr ',' ' '); do 176 [ -z "$w" ] && continue 177 # variante en formato original 178 dest="$OUT/img/$base-$w.$ext" 179 if needs_update "$img" "$dest"; then 180 "$IM" "$img" -resize "${w}x>" -quality "$quality" \ 181 -strip "$dest" 2>/dev/null 182 gen=$((gen+1)) 183 else 184 cached=$((cached+1)) 185 fi 186 # variantes en formatos extra (webp, avif, ...) 187 for fmt in $(printf '%s' "$extras" | tr ',' ' '); do 188 [ -z "$fmt" ] && continue 189 destx="$OUT/img/$base-$w.$fmt" 190 if needs_update "$img" "$destx"; then 191 "$IM" "$img" -resize "${w}x>" -quality "$quality" \ 192 -strip "$destx" 2>/dev/null 193 gen=$((gen+1)) 194 else 195 cached=$((cached+1)) 196 fi 197 done 198 done 199 done 200 echo "imagenes: $gen generadas, $cached en cache" 201 } 202 203 cmd_stats() { 204 echo "=== simpleblog stats ===" 205 echo 206 if [ -d "$IMAGES" ]; then 207 orig_bytes=$(find "$IMAGES" -type f -exec wc -c {} + | awk 'END{print $1+0}') 208 orig_count=$(find "$IMAGES" -type f | wc -l) 209 printf 'originales (images/): %s ficheros, %s\n' \ 210 "$orig_count" "$(human_bytes "$orig_bytes")" 211 fi 212 if [ -d "$OUT" ]; then 213 out_bytes=$(find "$OUT" -type f -exec wc -c {} + | awk 'END{print $1+0}') 214 out_count=$(find "$OUT" -type f | wc -l) 215 printf 'salida (public/): %s ficheros, %s\n' \ 216 "$out_count" "$(human_bytes "$out_bytes")" 217 if [ -d "$OUT/img" ]; then 218 img_bytes=$(find "$OUT/img" -type f -exec wc -c {} + | awk 'END{print $1+0}') 219 img_count=$(find "$OUT/img" -type f | wc -l) 220 printf ' variantes de imagen: %s ficheros, %s\n' \ 221 "$img_count" "$(human_bytes "$img_bytes")" 222 fi 223 fi 224 echo 225 [ -d "$POSTS" ] && printf 'posts: %s\n' "$(find "$POSTS" -type f \( -name '*.md' -o -name '*.html' \) | wc -l)" 226 [ -d "$PAGES" ] && printf 'pages: %s\n' "$(find "$PAGES" -type f \( -name '*.md' -o -name '*.html' \) | wc -l)" 227 [ -d "$OUT/tags" ] && printf 'tags: %s\n' "$(find "$OUT/tags" -name '*.html' | wc -l)" 228 } 229 230 # bytes -> "12 KB" / "3,4 MB" 231 human_bytes() { 232 awk -v b="$1" 'BEGIN{ 233 if (b<1024) { printf "%d B", b; exit } 234 if (b<1024*1024) { printf "%.1f KB", b/1024; exit } 235 printf "%.1f MB", b/1024/1024 236 }' 237 } 238 239 # --- comandos ------------------------------------------------------------ 240 241 cmd_clean() { 242 rm -rf "$OUT" 243 mkdir -p "$OUT" 244 } 245 246 # init: copia $DEFAULTS/skel/ al directorio actual. 247 cmd_init() { 248 if [ ! -d "$DEFAULTS/skel" ]; then 249 echo "no encuentro el skel en $DEFAULTS/skel" >&2 250 echo "define SIMPLEBLOG_DEFAULTS o instala simpleblog" >&2 251 exit 1 252 fi 253 # avisamos si el directorio no esta vacio (ignoramos .git) 254 if [ -n "$(ls -A "$SITE_DIR" 2>/dev/null | grep -v '^\.git$' || true)" ]; then 255 echo "el directorio $SITE_DIR no esta vacio. usa otro o limpia primero." >&2 256 exit 1 257 fi 258 cp -r "$DEFAULTS/skel"/. "$SITE_DIR"/ 259 echo "sitio inicializado en $SITE_DIR" 260 echo "siguiente paso: edita config y posts/, luego 'simpleblog build'" 261 } 262 263 cmd_serve() { 264 port="${SIMPLEBLOG_PORT:-8080}" 265 [ -d "$OUT" ] || cmd_build 266 echo "serving $OUT/ on http://localhost:$port" 267 if command -v darkhttpd >/dev/null 2>&1; then 268 exec darkhttpd "$OUT" --port "$port" 269 elif command -v httpd >/dev/null 2>&1; then 270 exec httpd -f -p "$port" -h "$OUT" 271 elif command -v busybox-extras >/dev/null 2>&1; then 272 exec busybox-extras httpd -f -p "$port" -h "$OUT" 273 elif command -v busybox >/dev/null 2>&1 && busybox --list 2>/dev/null | grep -qx httpd; then 274 exec busybox httpd -f -p "$port" -h "$OUT" 275 elif command -v python3 >/dev/null 2>&1; then 276 cd "$OUT" && exec python3 -m http.server "$port" 277 else 278 echo "no httpd encontrado. instala darkhttpd o busybox-extras." >&2 279 exit 1 280 fi 281 } 282 283 # escribe en $tmp/common.vars las variables que comparten todos los layouts 284 # (excepto page_title/page_description, que dependen de cada pagina). 285 write_common_vars() { 286 { 287 printf 'title\t%s\n' "$(sed_escape "$site_title")" 288 printf 'description\t%s\n' "$(sed_escape "$site_desc")" 289 printf 'author\t%s\n' "$(sed_escape "$site_author")" 290 printf 'baseurl\t%s\n' "$(sed_escape "$site_baseurl")" 291 printf 'siteurl\t%s\n' "$(sed_escape "$site_url")" 292 printf 'feed\t%s\n' "$(sed_escape "$site_feed")" 293 printf 'lang\t%s\n' "$(sed_escape "$site_lang")" 294 } > "$1" 295 } 296 297 # render_layout body_file out_file page_title page_description 298 # concatena header.html + body + footer.html, sustituyendo variables comunes 299 # (incluyendo {{menu}} desde $tmp/menu.html). 300 render_layout() { 301 bodyf="$1"; outf="$2"; pt="$3"; pd="$4" 302 vars="$tmp/layout.vars" 303 cp "$tmp/common.vars" "$vars" 304 { 305 printf 'page_title\t%s\n' "$(sed_escape "$pt")" 306 printf 'page_description\t%s\n' "$(sed_escape "$pd")" 307 } >> "$vars" 308 render "$(tpl header.html)" "$vars" "$tmp/_layout.header" 309 render "$(tpl footer.html)" "$vars" "$tmp/_layout.footer" 310 # {{menu}} en el header se inserta como bloque (HTML, contiene &/<>) 311 sub_file menu "$tmp/menu.html" "$tmp/_layout.header" 312 cat "$tmp/_layout.header" "$bodyf" "$tmp/_layout.footer" > "$outf" 313 } 314 315 cmd_build() { 316 mkdir -p "$OUT" 317 318 site_title=$(conf title) 319 site_desc=$(conf description) 320 site_author=$(conf author) 321 site_baseurl=$(conf baseurl) 322 site_url=$(conf siteurl) 323 site_lang=$(conf lang) 324 site_feed=$(conf feed) 325 posts_per_page=$(conf posts_per_page) 326 [ -z "$posts_per_page" ] && posts_per_page=0 327 md_cmd=$(conf markdown_cmd) 328 [ -z "$md_cmd" ] && md_cmd=smu 329 # rutas relativas (./foo) se resuelven respecto al sitio 330 case "$md_cmd" in 331 ./*) md_cmd="$SITE_DIR/${md_cmd#./}" ;; 332 esac 333 334 tmp=$(mktemp -d) 335 trap 'rm -rf "$tmp"' EXIT 336 337 write_common_vars "$tmp/common.vars" 338 : > "$tmp/menu.html" # vacio por defecto (se sobrescribe abajo) 339 340 drafts=0 341 : > "$tmp/menu_items.txt" # order<TAB>title<TAB>slug 342 : > "$tmp/index_items.txt" # date<TAB>slug<TAB>url<TAB>title<TAB>description 343 : > "$tmp/tag_items.txt" # tag<TAB>date<TAB>slug<TAB>url<TAB>title<TAB>description 344 345 # === FASE 1: paginas estaticas (pages/) === 346 # genera el HTML del cuerpo de cada pagina y recolecta items del menu. 347 # el render completo se hace despues (fase 2) cuando ya tenemos el menu. 348 if [ -d "$PAGES" ]; then 349 for src in "$PAGES"/*.md "$PAGES"/*.html; do 350 [ -e "$src" ] || continue 351 name=${src##*/}; ext=${name##*.}; slug=${name%.*} 352 353 body="$tmp/p.$slug.body.src"; meta="$tmp/p.$slug.meta" 354 : > "$body"; : > "$meta" 355 parse_post "$src" "$body" "$meta" 356 357 if [ "$(meta_get draft "$meta")" = "true" ]; then 358 drafts=$((drafts + 1)); continue 359 fi 360 custom_slug=$(meta_get slug "$meta") 361 [ -n "$custom_slug" ] && slug="$custom_slug" 362 363 title=$(meta_get title "$meta") 364 [ -z "$title" ] && title="$slug" 365 desc=$(meta_get description "$meta") 366 367 if [ "$ext" = "html" ]; then 368 cp "$body" "$tmp/p.$slug.body.html" 369 else 370 "$md_cmd" < "$body" > "$tmp/p.$slug.body.html" 371 fi 372 373 # marcador en disco con metadatos de la pagina para fase 2 374 printf '%s\t%s\t%s\n' "$slug" "$title" "$desc" >> "$tmp/pages_list.txt" 375 376 # acumular en menu si procede 377 if [ "$(meta_get menu "$meta")" = "true" ]; then 378 order=$(meta_get menu_order "$meta") 379 [ -z "$order" ] && order=100 380 printf '%s\t%s\t%s\n' "$order" "$title" "$slug" \ 381 >> "$tmp/menu_items.txt" 382 fi 383 done 384 fi 385 386 # construir HTML del menu a partir de menu_items.txt (orden numerico) 387 if [ -s "$tmp/menu_items.txt" ]; then 388 sort -n "$tmp/menu_items.txt" | while IFS=' ' read -r o t s; do 389 printf '<a href="%s%s.html">%s</a>\n' \ 390 "$site_baseurl" "$s" "$t" 391 done > "$tmp/menu.html" 392 fi 393 394 # === FASE 2: render final de paginas estaticas (ya con el menu) === 395 if [ -s "$tmp/pages_list.txt" ]; then 396 while IFS=' ' read -r slug title desc; do 397 pvars="$tmp/p.$slug.vars" 398 { printf 'title\t%s\n' "$(sed_escape "$title")"; } > "$pvars" 399 cp "$(tpl page.html)" "$tmp/p.$slug.body" 400 while IFS=' ' read -r k v; do 401 sub_var "$k" "$v" "$tmp/p.$slug.body" 402 done < "$pvars" 403 sub_file content "$tmp/p.$slug.body.html" "$tmp/p.$slug.body" 404 render_layout "$tmp/p.$slug.body" "$OUT/$slug.html" \ 405 "$title — $site_title" "${desc:-$site_desc}" 406 done < "$tmp/pages_list.txt" 407 fi 408 409 # === FASE 3: posts === 410 for src in "$POSTS"/*.md "$POSTS"/*.html; do 411 [ -e "$src" ] || continue 412 name=${src##*/}; ext=${name##*.}; slug=${name%.*} 413 414 body="$tmp/$slug.body.src"; meta="$tmp/$slug.meta" 415 : > "$body"; : > "$meta" 416 parse_post "$src" "$body" "$meta" 417 418 if [ "$(meta_get draft "$meta")" = "true" ]; then 419 drafts=$((drafts + 1)); continue 420 fi 421 custom_slug=$(meta_get slug "$meta") 422 [ -n "$custom_slug" ] && slug="$custom_slug" 423 424 title=$(meta_get title "$meta") 425 date=$(meta_get date "$meta") 426 desc=$(meta_get description "$meta") 427 tags_raw=$(meta_get tags "$meta") 428 [ -z "$title" ] && title="$slug" 429 430 if [ "$ext" = "html" ]; then 431 cp "$body" "$tmp/$slug.body.html" 432 else 433 "$md_cmd" < "$body" > "$tmp/$slug.body.html" 434 fi 435 436 # HTML de tags para mostrar en el propio post + acumular en tag_items 437 tags_html="" 438 if [ -n "$tags_raw" ]; then 439 tags_html='<span class="post-tags">' 440 for tg in $(split_tags "$tags_raw"); do 441 tags_html="$tags_html<a href=\"${site_baseurl}tags/${tg}.html\">$tg</a> " 442 printf '%s\t%s\t%s\t%s\t%s\t%s\n' \ 443 "$tg" "$date" "$slug" "$slug.html" "$title" "${desc:-}" \ 444 >> "$tmp/tag_items.txt" 445 done 446 tags_html="${tags_html}</span>" 447 fi 448 449 # render post body 450 pvars="$tmp/$slug.pvars" 451 { 452 printf 'title\t%s\n' "$(sed_escape "$title")" 453 printf 'date\t%s\n' "$(sed_escape "$date")" 454 printf 'tags\t%s\n' "$(sed_escape "$tags_html")" 455 } > "$pvars" 456 cp "$(tpl post.html)" "$tmp/$slug.body" 457 while IFS=' ' read -r k v; do 458 sub_var "$k" "$v" "$tmp/$slug.body" 459 done < "$pvars" 460 sub_file content "$tmp/$slug.body.html" "$tmp/$slug.body" 461 462 render_layout "$tmp/$slug.body" "$OUT/$slug.html" \ 463 "$title — $site_title" "${desc:-$site_desc}" 464 465 printf '%s\t%s\t%s\t%s\t%s\n' "$date" "$slug" "$slug.html" "$title" "${desc:-}" \ 466 >> "$tmp/index_items.txt" 467 done 468 469 # === FASE 4: indice (con paginacion) === 470 sort -r "$tmp/index_items.txt" > "$tmp/index_items.sorted" 471 472 total=$(wc -l < "$tmp/index_items.sorted" | tr -d ' ') 473 if [ "$posts_per_page" -le 0 ] || [ "$total" -le "$posts_per_page" ]; then 474 # una sola pagina 475 render_index_page "$tmp/index_items.sorted" "Entradas" "" "" "$OUT/index.html" 476 else 477 mkdir -p "$OUT/page" 478 page=1; start=1 479 while [ "$start" -le "$total" ]; do 480 end=$((start + posts_per_page - 1)) 481 [ "$end" -gt "$total" ] && end="$total" 482 chunk="$tmp/index_chunk_$page" 483 sed -n "${start},${end}p" "$tmp/index_items.sorted" > "$chunk" 484 485 # enlaces prev/next 486 prev="" 487 next="" 488 if [ "$page" -gt 1 ]; then 489 if [ "$page" -eq 2 ]; then 490 prev="<a href=\"${site_baseurl}\">« mas recientes</a>" 491 else 492 prev="<a href=\"${site_baseurl}page/$((page-1)).html\">« mas recientes</a>" 493 fi 494 fi 495 if [ "$end" -lt "$total" ]; then 496 next="<a href=\"${site_baseurl}page/$((page+1)).html\">mas antiguas »</a>" 497 fi 498 499 if [ "$page" -eq 1 ]; then 500 outf="$OUT/index.html" 501 else 502 outf="$OUT/page/$page.html" 503 fi 504 render_index_page "$chunk" "Entradas" "$prev" "$next" "$outf" 505 506 start=$((end + 1)); page=$((page + 1)) 507 done 508 fi 509 510 # === FASE 5: pagina por tag === 511 if [ -s "$tmp/tag_items.txt" ]; then 512 mkdir -p "$OUT/tags" 513 cut -f1 "$tmp/tag_items.txt" | sort -u > "$tmp/tags_unique.txt" 514 while IFS= read -r tag; do 515 awk -F'\t' -v t="$tag" 'BEGIN{OFS=FS} $1==t {print $2,$3,$4,$5,$6}' \ 516 "$tmp/tag_items.txt" | sort -r > "$tmp/tag_$tag.sorted" 517 render_index_page "$tmp/tag_$tag.sorted" "Tag: $tag" "" "" \ 518 "$OUT/tags/$tag.html" 519 done < "$tmp/tags_unique.txt" 520 fi 521 522 # === FASE 6: indice global de tags === 523 if [ -s "$tmp/tag_items.txt" ]; then 524 : > "$tmp/tags_index.html" 525 cut -f1 "$tmp/tag_items.txt" | sort | uniq -c | sort -rn | \ 526 while read -r count tag; do 527 printf ' <li><a href="%stags/%s.html">%s</a> <small>(%s)</small></li>\n' \ 528 "$site_baseurl" "$tag" "$tag" "$count" >> "$tmp/tags_index.html" 529 done 530 cp "$(tpl tags.html)" "$tmp/tags.body" 531 sub_file items "$tmp/tags_index.html" "$tmp/tags.body" 532 render_layout "$tmp/tags.body" "$OUT/tags.html" \ 533 "Tags — $site_title" "Indice de tags" 534 fi 535 536 # === FASE 7: feed atom === 537 if [ -n "$site_feed" ]; then 538 feed_updated="" 539 read -r first < "$tmp/index_items.sorted" || true 540 if [ -n "$first" ]; then 541 d=$(printf '%s' "$first" | cut -f1) 542 feed_updated=$(to_rfc3339 "$d") 543 fi 544 [ -z "$feed_updated" ] && feed_updated=$(date -u +%Y-%m-%dT%H:%M:%SZ) 545 546 fvars="$tmp/feed.vars" 547 { 548 printf 'title\t%s\n' "$(sed_escape "$(xml_escape "$site_title")")" 549 printf 'description\t%s\n' "$(sed_escape "$(xml_escape "$site_desc")")" 550 printf 'author\t%s\n' "$(sed_escape "$(xml_escape "$site_author")")" 551 printf 'baseurl\t%s\n' "$(sed_escape "$site_baseurl")" 552 printf 'siteurl\t%s\n' "$(sed_escape "$site_url")" 553 printf 'feed\t%s\n' "$(sed_escape "$site_feed")" 554 printf 'feed_updated\t%s\n' "$(sed_escape "$feed_updated")" 555 } > "$fvars" 556 render "$(tpl atom-header.xml)" "$fvars" "$tmp/feed.header" 557 558 : > "$tmp/feed.items" 559 while IFS=' ' read -r d s u t desc; do 560 [ -z "$u" ] && continue 561 updated=$(to_rfc3339 "$d") 562 absurl="$site_url$site_baseurl$u" 563 564 evars="$tmp/e.vars" 565 { 566 printf 'title\t%s\n' "$(sed_escape "$(xml_escape "$t")")" 567 printf 'absurl\t%s\n' "$(sed_escape "$(xml_escape "$absurl")")" 568 printf 'updated\t%s\n' "$(sed_escape "$updated")" 569 printf 'description\t%s\n' "$(sed_escape "$(xml_escape "$desc")")" 570 } > "$evars" 571 cp "$(tpl atom-item.xml)" "$tmp/e.out" 572 while IFS=' ' read -r k v; do 573 sub_var "$k" "$v" "$tmp/e.out" 574 done < "$evars" 575 if [ -f "$tmp/$s.body.html" ]; then 576 sub_file content "$tmp/$s.body.html" "$tmp/e.out" 577 else 578 sub_var content "" "$tmp/e.out" 579 fi 580 cat "$tmp/e.out" >> "$tmp/feed.items" 581 done < "$tmp/index_items.sorted" 582 583 render "$(tpl atom-footer.xml)" "$fvars" "$tmp/feed.footer" 584 cat "$tmp/feed.header" "$tmp/feed.items" "$tmp/feed.footer" > "$OUT/$site_feed" 585 fi 586 587 # === FASE 8: imagenes (originales en images/ -> public/img/*) === 588 process_images 589 590 # === FASE 9: static tal cual === 591 if [ -d "$STATIC" ]; then 592 cp -r "$STATIC"/. "$OUT"/ 593 fi 594 595 if [ "$drafts" -gt 0 ]; then 596 echo "build ok -> $OUT (saltados $drafts borradores)" 597 else 598 echo "build ok -> $OUT" 599 fi 600 } 601 602 # render_index_page items_file list_title prev_link next_link out_file 603 # items_file: lineas date<TAB>slug<TAB>url<TAB>title<TAB>description 604 render_index_page() { 605 items="$1"; ltitle="$2"; prev="$3"; next="$4"; outf="$5" 606 607 : > "$tmp/_items_html" 608 while IFS=' ' read -r d s u t desc; do 609 [ -z "$u" ] && continue 610 ivars="$tmp/_i.vars" 611 { 612 printf 'date\t%s\n' "$(sed_escape "$d")" 613 printf 'url\t%s\n' "$(sed_escape "$site_baseurl$u")" 614 printf 'title\t%s\n' "$(sed_escape "$t")" 615 printf 'description\t%s\n' "$(sed_escape "$desc")" 616 } > "$ivars" 617 render "$(tpl index-item.html)" "$ivars" "$tmp/_i.out" 618 cat "$tmp/_i.out" >> "$tmp/_items_html" 619 done < "$items" 620 621 cp "$(tpl index.html)" "$tmp/_index.body" 622 sub_var list_title "$(sed_escape "$ltitle")" "$tmp/_index.body" 623 pag="$prev $next" 624 sub_var pagination "$(sed_escape "$pag")" "$tmp/_index.body" 625 sub_file items "$tmp/_items_html" "$tmp/_index.body" 626 627 render_layout "$tmp/_index.body" "$outf" "$ltitle — $site_title" "$site_desc" 628 } 629 630 # --- entrada ------------------------------------------------------------- 631 632 case "${1:-build}" in 633 init) cmd_init ;; 634 build) cmd_build ;; 635 clean) cmd_clean ;; 636 stats) cmd_stats ;; 637 serve) cmd_serve ;; 638 *) echo "uso: $0 [init|build|clean|stats|serve]" >&2; exit 1 ;; 639 esac