Autoblog de gordon.re

Ce site n'est pas le site officiel de gordon.re
C'est un blog automatisé qui réplique les articles de gordon.re

Bonjour Axolotl !

2013-04-07T00:00:00+02:00 - (source)

Il y a quelques semaines, on m’a parlé de la possibilité d’adopter des axolotls. Je connaissais vaguement ces bestioles, mais la proposition m’a donné envie d’en apprendre plus sur elles. Après lecture de la page Wikipedia, je suis tombé sous le charme de ces bestioles. Voyez par vous-mêmes : une bestiole qui sourit tout le temps, qui passe sa vie sous forme de larve, qui peut se regénérer jusqu’à des parties de son cerveau (Arme X ?), qui a des branchies rigolotes et poilues, et surtout, qui mange comme un idiot tout ce qui est plus petit que lui. Je trouve ça génial.

Un axolotl

J’ai rapidement eu l’idée de monter un petit site, dans la lignée des Bonjour le chat et autres variations plus portées sur les humain·e·s, c’est à dire un blog simpliste postant une photo chaque matin. La quasi-totalité de ces sites est hébergée et gérée par Tumblr, mais vous connaissez mon amour pour les plateformes centralisées et pseudo-gratuites. Je suis donc parti sur Pelican, comme pour mon blog, avec dans l’idée la possibilité d’automatiser la création de posts.

Le travail a donc consisté à :

Installation

Le bon sens et la conception de Pelican veulent que le contenu soit généré depuis le poste local, pour être envoyé vers le serveur sans qu’il ne soit nécessaire d’avoir un interpréteur Python sur celui-ci. Dans mon cas, souhaitant une génération automatique, il m’a semblé plus propice de tout déléguer au serveur. Ainsi, j’ai installé Pelican sur mon serveur en suivant le guide, et configuré le serveur web pour servir le dossier output.

Je me suis aperçu après coup qu’une fonctionnalité intéressante me manquait dans Pelican : la possibilité, pour chaque article, d’avoir le lien vers son prédécesseur et/ou successeur, pour obtenir une navigation simple. Ce fonctionnement peut être obtenu via un plugin : Neighbors. Celui-ci, une fois téléchargé, se place à la racine du projet Pelican, et on l’inclut via le fichier de configuration, que voici :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/usr/bin/env python
# -*- coding: utf-8 -*- #

AUTHOR = u'Axoloto'
SITENAME = u'Bonjour Axolotl'
SITESUBTITLE = u'Le site qui vous fera dire « Bonjour l’<a '\
              +u'href="https://fr.wikipedia.org/wiki/Axolotl">axolotl</a> ! »'\
              +u'<br /><br />Tous les matins, 10h, une nouvelle <strong>photo'\
              +u'</strong> d’<strong>axolotl</strong> !'
SITEURL = 'http://bonjouraxolotl.fr'

TIMEZONE = 'Europe/Paris'

DEFAULT_LANG = u'fr'

# Blogroll
LINKS =  (('Pelican', 'http://docs.notmyidea.org/alexis/pelican/'),
          ('Python.org', 'http://python.org'),
          ('Jinja2', 'http://jinja.pocoo.org'),
          ('You can modify those links in your config file', '#'),)

# Social widget
SOCIAL = (('You can add links in your config file', '#'),
                  ('Another social link', '#'),)

DEFAULT_PAGINATION = 1

THEME = 'template'
FEED_DOMAIN = 'http://bonjouraxolotl.fr'
FEED_RSS = '/feeds'
FEED_ATOM =  None

PLUGINS = ["neighbors"]

Dans les quelques trucs intéressants, notez que j’ai restreint les pages à 1 seul article, et que j’ai inclus le plugin à la dernière ligne. Le reste est très simple. J’ai aussi choisi d’utiliser du RSS au lieu d’Atom.

Création du template

Je me suis basé sur le markup généré par bonjourlechat.fr pour créer mon template. Il n’y a basiquement besoin que de 3 pages : la page d’accueil, la page d’un article, et la page d’archives. D’ailleurs, je ne me suis même pas préoccupé de cette dernière…

Pour commencer, j’ai copié le template notmyidea, situé dans $PELICAN_INSTALL_PATH/themes/notmyidea, dans la racine du projet, renommé en « template ». À l’intérieur de celui-ci, on trouve deux dossiers : static et templates. Le premier sera copié tel quel dans le dossier output, tandis que le second contient le code Jinja permettant de générer les pages. Celles qui nous intéressent sont base.html, index.html, article.html et article_infos.html. La différence entre les deux derniers est que article.html est la page complète d’un article tandis que article_infos* n’est que le bloc HTML d’un article seul. Je l’ai mis dans un fichier à part pour pouvoir l’inclure facilement depuis les différentes pages sans avoir besoin de le réécrire. voici le contenu des fichiers :

base.html

<!DOCTYPE html>
<html lang="en">
<head>
        <title>{% block title %}{{ SITENAME }}{%endblock%}</title>
        <meta charset="utf-8" />
        <link rel="stylesheet" href="{{ SITEURL }}/theme/css/{{ CSS_FILE }}" type="text/css" />
        {% if FEED_ALL_ATOM %}
        <link href="{{ FEED_DOMAIN }}/{{ FEED_ALL_ATOM }}" type="application/atom+xml" rel="alternate" title="{{ SITENAME }} Atom Feed" />
        {% endif %}
        {% if FEED_ALL_RSS %}
        <link href="{{ FEED_DOMAIN }}/{{ FEED_ALL_RSS }}" type="application/rss+xml" rel="alternate" title="{{ SITENAME }} RSS Feed" />
        {% endif %}
</head>

<body id="index" class="home">
    <div id="wrapper">
        <div id="topNav">
            <ul>
                <li><a href="/archives.html">Archives</a></li>
                <li><a href="{{ FEED_DOMAIN }}/{{ FEED_ADD_RSS }}">RSS</a></li>
            </ul>
        </div>
        <div id="contentHolder">
            <div id="mastHead">
                <h1><a href="{{ SITEURL }}/">{{ SITENAME }}</a></h1>
                <p>
                    <span style="font-family: Arial">{{ SITESUBTITLE }}</span>
                </p>
            </div>
            <div id="content">
                <div id="postHolder">
                    {% block content %}
                    {% endblock %}
                </div>
            </div>
        </div>
        <div id="footer">
            <p style="text-align:center">
                Fièrement propulsé par <a href="http://getpelican.com/">Pelican</a>,
                qui se repose sur <a href="http://python.org">Python</a>.
            </p>

            <p style="text-align: center">Thème par
            <a href="http://daelan.com/">Daelan</a>, merci !</p>
        </div><!-- /#contentinfo -->
    </div>
</body>
</html>

article.html

{% extends "base.html" %}
{% block title %}{{ article.title|striptags }}{{ SITENAME }}{%
    endblock %}
{% block content %}
    {% include 'article_infos.html' %}
{% endblock %}

article_infos.html

<div class="post">
    <div class="labels">
        <div class="date">
            <a href="{{ SITEURL }}/{{ article.url }}">
                <span class="month">{{ article.date.strftime('%d %m') }}</span> 
                <span class="year">{{ article.date.year }}</span>
            </a>
        </div>
        <div id="navigation" style="position: absolute; top:0; right: 0;">
            {% if article.prev_article %}
                <a href="{{ article.prev_article.url }}"><b>«</b></a>
            {% endif %}
            {% if article.next_article %}
                <a href="{{ article.next_article.url }}"><b>»</b></a>
            {% endif %}
        </div>
    </div>
    <div class="photo">
        <div class="permalink">
            <a href="{{ SITEURL }}/{{ article.url }}">+</a>
            {{ article.content }}

            <div class="caption"><p>{{ article.title }}</p></div>
        </div>
    </div>
</div>

Un dernier détail : le CSS. J’ai copié bêtement le CSS du site cible, que j’ai mis dans template/css/main.css :

/* -- reset this mutha!  -- */
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, font, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td {
    margin: 0;
    padding: 0;
    border: 0;
    outline: 0;
    font-size: 100%;
    vertical-align: baseline;
    background: transparent;
}

body {
    line-height: 1;
}

ol, ul {
    list-style: none;
}

blockquote, q {
    quotes: none;
}

blockquote:before, blockquote:after,
q:before, q:after {
    content: '';
    content: none;
}

/* remember to define focus styles! */
:focus {
    outline: 0;
}

/* remember to highlight inserts somehow! */
ins {
    text-decoration: none;
}

del {
    text-decoration: line-through;
}

/* -- end reset  -- */

body{
    margin:0;
    padding:6px;
    text-align:center;
    font-family: Helvetica, Arial, Verdana, Sans-Serif;
    background: #fff;
}

#wrapper{
    width:895px;
    height:auto;
    margin:0 auto;
    padding:0;
    text-align:left;
}

#topNav{
    position:relative;
    float:left;
    width:895px;
    height:90px;
    margin:0;
    padding:0;
}

#topNav ul{
    position:relative;
    float:right;
    margin:0 0 0 0;
}

#topNav ul li{
    position:relative;
    float:left;
    margin:0 0 0 20px;
}

#topNav ul li a:link,#topNav ul li a:visited{
    font-size:12px;
    color:#000000;
    text-decoration:none;
    background:#FFFFFF;
    border:0;
    padding:3px 6px 3px 6px;
    line-height:18px;
}

#topNav ul li a:hover{
    color:#FFFFFF;
    background:#0000FF;
}

#contentHolder{
    position:relative;
    float:left;
    width:895px;
    height:auto;
    min-height: 500px;
}

#mastHead{
    position:relative;
    float:left;
    width:395px;
    height:auto;
    margin:0 0 0 0;
    display:inline;
}

#mastHead h1{
    margin:9px 18px 18px 0;
}

#mastHead h1 a:link,#mastHead h1 a:visited{
    letter-spacing:-2px;
    font-size:36px;
    background:#0000FF;
    padding:18px;
    line-height:54px;
    border:0;
    text-decoration:none;
    color:#FFFFFF;
}

#mastHead h1 a:hover{
    background:#0000FF;
    color:#000000;
}

#mastHead p{
    font-family:Georgia, "Times New Roman", Serif;
    font-family:#000000;
    font-size:11px;
    padding:9px 36px 9px 18px;
    line-height:18px;
    margin:0;
}

#mastHead form{
    padding:9px 18px 9px 18px;
}

#mastHead input{
    padding:0;
    margin:0;
}

#mastHead a:link, #mastHead a:visited{
    text-decoration:none;
    color:#0000FF;
    border-bottom: 1px dotted #FFFFFF;
}

#mastHead a:hover{
    text-decoration:none;
    color:#0000FF;
    border-bottom: 1px dotted #FFFFFF;
}

div.photo img{
    border:0;
}

a.hyperLink:link,a.hyperLink:visited{
    color:#0000FF;
    text-decoration:none;
    font-size:18px;
    line-height:18px;
    font-weight:bold;
    border:0;
}

a.hyperLink:hover{
    color:#FFFFFF;
}

.description{
    position:relative;
    float:left;
    width:100%;
    height:auto;
    margin:18px 0 18px 0;
}

.post{
    position:relative;
    float:left;
    border-bottom:1px dotted #ccc;
    padding:0 0 18px 0;
    width:100%;
    height:auto;
    font-size:11px;
    line-height:18px;
    margin:0 0 18px 0;
}

.labels{
    margin:0;
    padding:0;
}

#content{
    position:relative;
    float:left;
    width:500px;
    height:auto;
    margin:0 0 0 0;
    display:inline;
}

#content p{
    font-size:11px;
    line-height:18px;
    margin:0 0 18px 0;
}

#content h2{
    position:relative;
    float:left;
    width:500px;
    padding:0 0 0 0;
    height:auto;
    line-height:18px;
    font-size:18px;
    margin:0 0 18px 0;
}

#content h2 a:link,#content h2 a:visited{
    color:#0000FF;
    text-decoration:none;
    border:0;
}

#content h2 a:hover{
    color:#FFFFFF;
}

.date{
    border-bottom:1px dotted #666;
    float:left;
    font-size:18px;
    font-weight:bold;
    letter-spacing:-1px;
    margin:0 0 18px 0;
    padding:0;
    position:relative;
    text-transform:uppercase;
    line-height:18px;
    width:500px;
}

.year{
    color:#0000ff;
    letter-spacing:0;
}

.month{
    color:#0000FF;
    letter-spacing:0;
}

#content a:link, #content a:visited{
    text-decoration:none;
    color:#0000FF;
    border-bottom: 1px dotted #FFFFFF;
}

#content a:hover{
    text-decoration:none;
    color:#0000FF;
    border-bottom: 1px dotted #FFFFFF;
}

#footer{
    position:relative;
    float:left;
    width:895px;
    height:auto;
    border-top:1px dotted #efefef;
    padding:18px 0 0 0;
    margin:18px 0 18px 0;
}

#footer p{
    font-size:11px;
    line-height:18px;
    margin:0 20px 0 20px;
}

#footer a:link, #footer a:visited{
    text-decoration:none;
    color:#000;
    border-bottom: 1px dotted #FFFFFF;
}

#footer a:hover{
    text-decoration:none;
    color:#0000FF;
    border-bottom: 1px dotted #FFFFFF;
}

.caption{
    position:relative;
    float:left;
    margin:18px 0 18px 0;
    width:100%;
    height:auto;
}

.regular, .quote, .video, .photo, .audio, .conversation, .link{
    position:relative;
    float:left;
    width:100%;
    height:auto;
}

big{
    font-size:24px;
    line-height:18px;
    padding:0 5px 0 0;
}

.source{
    color:#666;
}

.permalink{
    position:absolute;
    top:0;
    left:-18px;
    height:18px;
    margin:0;
    padding:0;
    line-height:18px;
}

#navigation {
    font-size: 22px;
    font-weight: bold;
}

#pages{
    font-weight:normal;
    color:#999;
    padding:0;
    font-size:11px;
    margin:10px 0 0 0;
}

Voilà tout pour la partie Pelican. Vous avez un beau site, vide mais beau. Maintenant, remplissons-le dynamiquement.

Le contenu

Les sites bonjour* ont généralement une légende sous chaque photo. Disons qu’on s’en fout. J’ai opté pour un fonctionnement minimaliste : on pose des images dans un dossier, et le script, appelé une fois par jour, prend la plus ancienne pour en faire un article puis la supprime.

# -*- coding: utf-8 -*-

import os
import locale
from datetime import date
import Image

locale.setlocale(locale.LC_TIME, 'fr_FR.utf-8')

content_dir = 'content'
content_image_dir = 'images'
source_dir = 'source'
max_size = (500, 500)

def create_article(filename):
    today = date.today()
    output_filename = '%s.jpg' % today.strftime('%Y-%m-%d')
    im = Image.open(os.path.join(source_dir, filename))
    im.thumbnail(max_size)
    im.save(os.path.join(content_dir, content_image_dir, output_filename))

    article_vars = {'today_str': date.today().strftime('%a %d %b %Y'),
                    'date': today.strftime('%Y-%m-%d'),
                    'output_filename': output_filename}

    article = """Title: %(today_str)s
Date: %(date)s

![%(today_str)s](./images/%(output_filename)s)""" % article_vars
    with open(os.path.join(content_dir, output_filename.replace('jpg', 'md')), 'w') as article_file:
        article_file.write(article)

file_list = os.listdir(source_dir)
oldest_file = None

for cur_file in file_list:
    cur_mtime = os.path.getmtime(os.path.join(source_dir, cur_file))
    if not oldest_file or cur_mtime < oldest_mtime:
        oldest_file = cur_file
        oldest_mtime = cur_mtime

create_article(oldest_file)
os.remove(os.path.join(source_dir, oldest_file))

Enregistrez ça dans la racine du site, sous le nom content_generator.py par exemple. C’est testé sous python 2.6 et 2.7. À chaque appel, il listera les fichiers dans source (vous devriez créer ce dossier), les classera par date de modification, puis dépilera la première pour en faire un article : il la redimensionnera aux dimensions voulues, puis écrira un article basique en Markdown, contenant seulement l’image et la date.

À ce niveau, le billet sera écrit mais pas encore publié. C’est un script bash qui s’en chargera, et qui sera appelé par cron. Enregistrez ce qui suit dans cron.sh à la racine du site :

1
2
3
4
5
6
#!/bin/bash

PATH="/usr/local/bin:/usr/bin:/bin"
cd /path/to/bonjouraxolotl
python content_generator.py
make html

Adaptez bien évidemment les chemins. Il ne reste plus qu’à éditer la crontab :

# crontab -l
0 10 * * * /path/to/bonjouraxolotl/cron.sh

Tous les matins à 10h, un nouvel axolotl !

EDIT : corrections et amélioration du script python par Rogdham, merci à lui.


Powered by VroumVroumBlog 0.1.32 - RSS Feed
Download config articles