{ "version": "https://jsonfeed.org/version/1.1", "title": "Thomas Citharel", "language": "fr", "home_page_url": "https://tcit.fr", "feed_url": "https://tcit.fr/feed.json", "description": "Je m’appelle Thomas, et je crée des outils web.", "author": { "name": "Thomas Citharel", "url": "" }, "items": [{ "id": "https://tcit.fr/post/dynamically-add-enum-values-in-elixir's-graphql-library-absinthe/", "url": "https://tcit.fr/post/dynamically-add-enum-values-in-elixir's-graphql-library-absinthe/", "title": "Dynamically add enum values in Elixir's GraphQL Library Absinthe", "content_html": "
Do you have a huge list of value acceptable for a GraphQL enum? Absinthe can dynamically extend the schema at runtime.
\nWhile adding a "category" feature to the events in Mobilizon, I wanted to restict the values this field could take. Usually I use an enum in the GraphQL schema definition for this kind of situation, for instance:
\n@desc \"The list of join options for an event\"
enum :event_join_options do
value(:free, description: \"Anyone can join and is automatically accepted\")
value(:restricted, description: \"Manual acceptation\")
value(:invite, description: \"Participants must be invited\")
end
\nThis uses the enum
macro and three value
macros. But in this case there would be a lot of options for value
, so it wouldn't make sense to have them all in the schema declaration.
Moreover, I want this list of categories to be extendable in the future by the instance administrator, who doesn't want to recompile to extend the list.
\nSo the Absinthe docs are far from being perfect, but you can find mention of custom schema manipulation. This page, with the help of a few Github issues and a test file, helped me see clear through this.
\nFor starters, let's create a new module to dynamically build our new enum. There's a few comments inline.
\ndefmodule Mobilizon.GraphQL.Schema.Custom.EnumTypes do
alias Absinthe.Blueprint.Schema
alias Absinthe.Schema.Notation
alias Absinthe.{Blueprint, Pipeline, Phase}
# The list of categories
# having an atom identifier and a string label
# This is currently being defined as a module attribute,
# but in the future it will be expanded by some admin
# configuration as well
@categories [
%{
id: :arts,
label: \"ARTS\"
},
# … a long list of categories
]
def pipeline(pipeline) do
# When to insert this override
Pipeline.insert_after(pipeline, Phase.Schema.TypeImports, __MODULE__)
end
# Adding this enum to the list of schema definitions
def run(blueprint = %Blueprint{}, _) do
%{schema_definitions: [schema]} = blueprint
new_enum = build_dynamic_enum()
schema =
Map.update!(schema, :type_definitions, fn type_definitions ->
[new_enum | type_definitions]
end)
{:ok, %{blueprint | schema_definitions: [schema]}}
end
# Building the enum itself
def build_dynamic_enum do
%Schema.EnumTypeDefinition{
name: \"EventCategory\",
identifier: :event_category,
module: __MODULE__,
__reference__: Notation.build_reference(__ENV__),
values:
# Create a `Schema.EnumValueDefinition`
# for each value in `@categories`
Enum.map(@categories, fn %{id: id, label: label} ->
%Schema.EnumValueDefinition{
identifier: id,
# The `value` can either be `id` or `label`
# depending whether what you want to be given
value: label,
name: label,
module: __MODULE__,
__reference__: Notation.build_reference(__ENV__)
}
end)
}
end
end
\nFinally, we just have to add in our schema definition file the following @pipeline_modifier
attribute:
defmodule Mobilizon.GraphQL.Schema do
use Absinthe.Schema
@pipeline_modifier Custom.EnumTypes
# [… our whole schema]
end
\nAnd boom, we can use our new enum just like this:
\nfield :create_event, type: :event do
# …
arg(:category,
:event_category,
# string or atom depending on what you use for value above
default_value: \"ARTS\",
description: \"The event's category\"
)
# …
end
\n",
"date_published": "2022-03-27T18:22:36Z"
},{
"id": "https://tcit.fr/post/imprimer-en-pdf/",
"url": "https://tcit.fr/post/imprimer-en-pdf/",
"title": "Imprimer en PDF",
"content_html": "Quand je veux imprimer un fichier PDF sur une imprimante jamais configurée, ça prend souvent des heures sans raison.
\nJe constate alors ce message dans les logs :
\nUse "pdftops-renderer" option (see README file) to use Ghostscript or MuPDF for the PDF -> PostScript conversion\n
\nGlobalement il me semble que c'est la conversion de PDF en un truc compréhensible par l'imprimante (Ghostscript, PostScript) qui rate.
\nSur le web, il est proposé d'utiliser pdftops
comme outil de rendu (qui semble utiliser Poppler
), mais ce qui marche le mieux pour moi c'est directement d'utiliser Ghostscript
.
lpadmin -p mon_imprimante -o pdftops-renderer-default=gs
\nOù mon_imprimante
est le nom de l'imprimante, que vous pouvez trouver avec lpstat -p
.
L'impression se fait alors quasi-instantanément.
\n", "date_published": "2022-01-17T09:20:00Z" },{ "id": "https://tcit.fr/post/faire-fonctionner-le-wifi-intel-sous-fedora-35/", "url": "https://tcit.fr/post/faire-fonctionner-le-wifi-intel-sous-fedora-35/", "title": "Faire fonctionner le wifi Intel sous Fedora 35", "content_html": "Si vous avez migré vers la version 35 de Fedora et que vous utilisez une carte Wi-Fi Intel, cela peut vous aider.
\nTout d'abord, voyons si vous êtes concerné :
\nlspci | grep "Network controller"\n02:00.0 Network controller: Intel Corporation Wi-Fi 6 AX200 (rev 1a)\n
\nSi la référence de votre carte correspond à AX2xx
, vous êtes probablement concerné.
En Octobre 2021 le microcode pour cette carte a été migré dans un paquet séparé du fameux linux-firmware
.
Il faut alors installer iwlax2xx-firmware
pour faire refonctionner le Wi-Fi. Un redémarrage et c'est reparti.
Gridsome est un framework pour générer des sites statiques à l'aide de VueJS et GraphQL.
\nOn parle aussi de framework JAMStack, pour Javascript, APIs et Markup. En gros, ce terme décrit simplement d'avoir la possibilité d'ajouter des fonctionnalités dynamiques par dessus votre site statique généré à partir de fichiers sources comme du Markdown, le HTML étant hydraté avec l'application Javascript.
\nIl n'y a pas de fonctionnalités statiques ici, j'ai juste besoin d'un blog, il n'y aura pas grande différence à l'utilisation si jamais vous avez le Javascript activé ou non dans votre navigateur[^1].
\nJ'ai choisi Gridsome car il utilise le framework VueJS avec lequel je suis plus familiarisé, mais c'est quasiment le même fonctionnement que GatsbyJS qui utilise ReactJS.
\nJ'avais eu l'occasion d'utiliser Gatsby pour entraide.chatons.org et j'avais trouvé le concept intéressant. Aujourd'hui, cet article que vous lisez est justement affiché dans une page d'un site statique généré par Gridsome.
\nComme indiqué sur la page de présentation de Gridsome : vous prenez des sources de données en entrée, vous effectuez des traitements sur les données (sélection, transformation, affichage) et vous déployez les fichiers HTML et assets produits.
\nOn commence par installer le module qui fournit la ligne de commande :
\nyarn global add @gridsome/cli
\nJ'utilise yarn
dans les commandes, mais vous pouvez bien sûr utiliser les équivalents npm
si c'est ce que vous préférez.
Puis on crée un nouveau projet :
\ngridsome create mon-site
\nCela récupère le modèle du projet, le met à jour avec le nom du projet, et installe les dépendances.
\nOn peut alors se mettre dans le dossier du projet nouvellement créé puis lance le serveur de développement, par défaut sur le port 8080.
\ncd mon-site
gridsome develop
\nVous devriez avoir une structure de projet équivalente voire égale à celle ci-dessous :
\n├── gridsome.config.js\n├── gridsome.server.js\n├── package.json\n├── README.md\n├── src\n│ ├── components\n│ │ └── README.md\n│ ├── favicon.png\n│ ├── layouts\n│ │ ├── Default.vue\n│ │ └── README.md\n│ ├── main.js\n│ ├── pages\n│ │ ├── About.vue\n│ │ ├── Index.vue\n│ │ └── README.md\n│ └── templates\n│ └── README.md\n├── static\n│ └── README.md\n└── yarn.lock\n
\nOn voit dans src/pages/
les fichiers About.vue
et Index.vue
, qui correspondent aux deux routes accessibles via la barre de navigation. Notez donc que le routage de base se fait via les fichiers disponibles dans /pages/
. Index.vue
correspond à la page d'accueil /
, et un fichier MonProjet.vue
donnerait une route /mon-projet
.
On a également le fichier Default.vue
dans /src/layouts/
. Les layouts sont utilisés pour envelopper vos pages avec des éléments communs aux pages, typiquement l'en-tête de la page, la barre de navigation, le pied-de-page… Dans nos pages, par exemple About.vue
, le contenu est enveloppé par l'élément <Layout>
:
<template>
<Layout>
<h1>About us</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Error doloremque omnis animi, eligendi magni a voluptatum, vitae, consequuntur rerum illum odit fugit assumenda rem dolores inventore iste reprehenderit maxime! Iusto.</p>
</Layout>
</template>
\nLe squelette de notre site étant exploré, voyons comment y insérer nos données. Gridsome utilise GraphQL, un langage de requêtes. Nous avons par exemple un premier exemple de requête GraphQL dans le fichier layouts/Default.vue
, où le nom du site (défini dans le fichier de configuration gridsome.config.js
) est récupéré avec la requête suivante :
query {
metadata {
siteName
}
}
\nLa variable siteName
est alors disponible via l'expression $static.metadata.siteName
dans notre template VueJS.
Pour importer nos propres données, prenons l'exemple basique de billets de blog sous forme de fichiers locaux au format Markdown.
\nInstallons les modules @gridsome/source-filesystem
afin de récupérer le contenu des fichiers et @gridsome/transformer-remark
afin de transformer notre contenu markdown en HTML et surtout convertir l'éventuel front matter YAML en métadonnées utilisables.
Nous allons ajouter la configuration donnée en exemple dans gridsome.config.js
:
module.exports = {
plugins: [
{
use: '@gridsome/source-filesystem',
options: {
typeName: 'BlogPost',
path: './content/blog/**/*.md',
}
}
],
templates: {
BlogPost: '/blog/:year/:month/:day/:title'
}
}
\nPuis créons l'arborescence content/blog/2020
et un fichier hello world.md
au bout avec le contenu de base suivant :
---
date: 2020-01-05
title: \"Hello World\"
---
Il s'agit de mon **premier** contenu !
\nOn redémarre le serveur de développement Gridsome car on a changé la configuration puis on ouvre le bac à sable GraphQL pour effectuer nos premières requêtes et voir le résultat facilement.
\nPar exemple, pour récupérer la liste de tous nos posts, on écrira :
\nquery {
allBlogPost {
edges {
node {
title,
date(format: \"DD/MM/YYYY\"),
path
}
}
}
}
\nSi on édite notre page Index.vue
, on peut rajouter cette requête dans un bloc <page-query>
et ensuite accéder aux données via $page.allBlogPost
, comme par comme dans l'exemple suivant où nous rajoutons une liste des billets au template :
<ul v-for=\"edge in $page.allBlogPost.edges\" :key=\"edge.node.id\">
<li></li>
</ul>
\nAjoutons ensuite un fichier BlogPost.vue
dans nos templates/
pour représenter ces entités:
<template>
<Layout>
<article>
<h1>{{ $page.blogPost.title }}</h1>
<span>{{ $page.blogPost.date }}</span>
<div v-html=\"$page.blogPost.content\"></div>
</article>
</Layout>
</template>
<page-query>
query BlogPost($path: String!) {
blogPost(path: $path) {
title,
date(format: \"DD/MM/YYYY\"),
content
path
}
}
</page-query>
\nComme on l'a vu, les données de la requête GraphQL blogPost
sont injectées dans la variable $page
accessible dans mon template. Maintenant que l'on a une page dédiée pour afficher chaque billet, ajoutons un lien depuis notre liste sur la page Index.vue
, en changeant notre <li>
en :
<li>
<g-link :to=\"edge.node.path\"></g-link>
</li>
\nLa balise <g-link>
crée lors du rendu des balises <a>
tout ce qu'il y a de plus basique, mais permet aussi le préchargement des liens (voir les notes de bas de page). En cliquant dessus, on arrive finalement sur notre page montrant un article en particulier.
Générons maintenant notre site avec gridsome build
, puis servons le contenu du dossier dist
, par exemple avec serve : serve dist
.
Je vous laisse vérifier que la navigation fonctionne bien et que le HTML produit contient bien notre contenu.
\nOn peut publier le contenu de dist
là où on le souhaite, voici un exemple de fichier de configuration pour le publier sur Gitlab Pages :
image: node:14
deploy:
stage: deploy
script:
- yarn install
- yarn build
- gzip -kv -6 -r public
artifacts:
paths:
- public
\nJe m'arrête là pour parler de Gridsome, mais j'ai quand même prévu de parler de comment gérer mes contenus sur ce site avec NetlifyCMS, et comment mettre en place un chroot
sur un serveur pour y faire un rsync depuis notre CI afin de déployer notre site.
[^1]: Une différence est que si le Javascript est activé, Gridsome est censé observer les liens internes actuellement affichés à l'écran (avec IntersectionObserver
) pour les précharger afin que les chargements de page soient effectués avant même le clic éventuel sur le lien. Les gains devraient rester minimaux étant donné qu'il n'y a qu'un contenu HTML à récupérer depuis le serveur.
Au vent, des landes de pierre…
\nMaintenant que cette chanson est en tête, je voulais juste partager quelques photos d'Irlande.
J'y étais en Juin, en road trip sur la plus grande partie du littoral de la moitié Nord.
\nVoici des photos de mes coins préférés, surtout dans le comté de Mayo, de Downpatrick Head à la vallée de Doolough, en passant par l'île d'Achill et en poussant jusqu'au parc national du Connemara.
Elles sont classées par ordre chronologique.
\n", "date_published": "2019-11-09T22:57:27Z" },{ "id": "https://tcit.fr/post/internationalisation-simple-des-dates-dans-le-navigateur/", "url": "https://tcit.fr/post/internationalisation-simple-des-dates-dans-le-navigateur/", "title": "Internationalisation simple des dates dans le navigateur", "content_html": "Nous allons voir dans cet article comment formatter des dates facilement dans le navigateur en évitant de faire appel à des bibliothèques tierces.
\nLorsque l'on veut manipuler des données temporelles avec Javascript dans le navigateur, on se repose souvent sur la bibliothèque Moment.js.
\nElle permet en effet de formater des dates à sa convenance :
moment().format('MMMM Do YYYY, HH:mm:ss');
// October 19th 2019, 19:04:56
\nmoment().format('dddd, MMMM do YYYY');
// Saturday, October 6th 2019
\nSi vous chargez aussi le support des différentes locales, ça peut vous donner ceci :
\nmoment.locale('fr');
moment().format('LLL');
// 19 octobre 2019 19:09
\nPlutôt pratique, n'est-ce pas ? Du coup Moment.js est très populaire (42k stars sur Github) et très utilisé (presque 2 millions d'installations journalières via npm, où plus de 36 000 paquets en sont dépendants ).
\nLe souci de Moment.js, c'est son poids énorme de 329Ko (non compressé, et avec toutes les locales).
\nDu coup ça fait un sacré module à charger (et il faut mieux qu'il soit chargé dès le début, c'est pas très joli quand les dates s'affichent une fois seulement que la page est chargée).
Y a quelques solutions pour améliorer cela, par exemple charger uniquement la locale de l'utilisateur qui peut être fournie par le backend en fonction de l'en-tête Accept-Language
.
\nOn peut ensuite éventuellement charger dynamiquement d'autres locales si jamais l'utilisateur⋅ice décide de changer de langue.
Cela dit, même en chargeant uniquement le cœur de Moment.js, on reste à plusieurs dizaines de kilo-octets pour une utilisé potentiellement très limitée.
\nEn réaction à cette problématique, les bibliothèques dayjs et date-fns - de poids beaucoup plus réduit - existent. Date-fns permet de faire du tree-shaking avec Webpack pour importer uniquement les modules dont on a besoin et dayjs n'incorpore que le minimum, en externalisant les fonctionnalités supplémentaires via des plugins.
\nPour la plupart des fonctionnalités, ça remplace très bien Moment.js. Vous pouvez voir un comparatif sur ce dépôt : You don't (may not) need Moment.js.
\nEt si on voulait s'épargner encore quelques kilo-octets, on regarderait du côté…de Javascript. En effet, il existe depuis relativement assez longtemps (2012) une API Intl
avec notamment un objet DateTimeFormat
fait pour formater des dates en fonction de la locale
.
Ça s'utilise comme cela :
\nlet date = new Date();
new Intl.DateTimeFormat('fr-FR').format(date);
// 10/19/2019
\nÇa prend en charge correctement les caractères non-latins et le sens d'écriture de droite à gauche :
\nlet date = new Date();
new Intl.DateTimeFormat('ar-EG').format(date);
// ١٩/١٠/٢٠١٩
\nSi on ne veut pas spécifier de locale, on peut passer l'argument 'default'
ou bien undefined
au constructeur de Intl.DateTimeFormat
, ça utilisera la locale configurée du navigateur :
let date = new Date();
new Intl.DateTimeFormat('default').format(date);
// 10/19/2019
\nBon, et maintenant si on veut une vraie date formattée, comment on fait ?
\nAvec le second argument du constructeur, pardi !
\nlet date = new Date();
let options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
new Intl.DateTimeFormat('default', options).format(date);
// samedi 19 octobre 2019
new Intl.DateTimeFormat('ar-EG', options).format(date);
// السبت، ١٩ أكتوبر ٢٠١٩
\nLa référence de ces options se trouve sur cette page du MDN.
\nComme l'API Intl est assez ancienne, le support est plutôt bon (Internet Explorer 11, Safari 10).
\nJe note qu'il existe aussi un objet Intl.RelativeTimeFormat
pour afficher des temps relatifs :
const rtf = new Intl.RelativeTimeFormat('default', { numeric: \"auto\" });
rtf.format(-1, \"day\");
// 'hier'
rtf.format(3, \"day\");
// 'dans 3 jours'
rtf.format(-6, \"month\");
// 'il y a 6 mois'
\nCette fois par contre, pas de support pour Internet Explorer, Edge ou Safari, ce qui rend son emploi plus hasardeux. On peut bien entendu détecter cela avec
\nif (Intl.DateTimeFormat) {
// do something
// it should probably work
}
if (Intl.RelativeTimeFormat) {
// do something
// you're using Firefox or Chrome
}
\nVoilà, même avec l'addition récente de RelativeTimeFormat
, l'API Intl
est vraiment loin d'avoir toutes les fonctionnalités de Moment.js ou même de date-fns, mais s'il vous faut juste afficher quelques dates au bon format, pas besoin de charger des centaines de kilooctets pour rien.🙂
En utilisant l'API Intl, on peut facilement récupérer la liste des mois et des noms des jours traduits dans la langue de l'utilisateur :
\nfunction localeMonthNames() {
const monthNames = [];
for (let i = 0; i < 12; i += 1) {
const d = new Date(2019, i, 1);
const month = d.toLocaleString('default', { month: 'long' });
monthNames.push(month);
}
return monthNames;
}
localeMonthNames();
// [ \"janvier\", \"février\", \"mars\", \"avril\", \"mai\", \"juin\", \"juillet\", … ]
\nLà, c'est plutôt simple, on n'a qu'à itérer chaque mois dans le constructeur de Date
pour avoir son nom textuel. On note quand même que les mois commencent à 0.
function localeShortWeekDayNames() {
const weekDayNames = [];
for (let i = 13; i < 20; i += 1) {
const d = new Date(2019, 9, i);
const weekDay = d.toLocaleString('default', { weekday: 'short' });
weekDayNames.push(weekDay);
}
return weekDayNames;
}
localeShortWeekDayNames();
// [ \"dim.\", \"lun.\", \"mar.\", \"mer.\", \"jeu.\", \"ven.\", \"sam.\" ]
\nIci, c'est plus compliqué, il faut partir d'une date où l'on sait qu'il s'agit d'un dimanche, puis itérer sur les 7 jours de la semaine. Et oui, le jour 0
est bien un Dimanche. Ici j'ai mis les versions courtes des noms des jours, mais ça marche aussi évidemment avec la fonction complète.
Ça peut éviter de traduire des informations inutiles.
\nUn truc qui manque par contre, c'est déterminer en fonction de la locale quel jour de la semaine celle-ci commence. En effet, si la fameuse norme ISO 8601 définit lundi comme le premier jour de la semaine, certains pays comme les États-Unis d'Amérique, le Canada, le Japon et l'Australie entament leur semaine le Dimanche. Au Moyen-Orient, elle commence le Samedi. Du coup si vous n'avez pas de bibliothèque tierce, il faut fournir l'option aux traducteurs pour qu'ils mettent leur propre valeur.
\nImage « Calendrier Mural Jours » par Andreas Lischka sur Pixabay
\n", "date_published": "2019-10-20T10:36:42Z" } ] }