A place to cache linked articles (think custom and personal wayback machine)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.html 38KB

hace 4 años
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762
  1. <!doctype html><!-- This is a valid HTML5 document. -->
  2. <!-- Screen readers, SEO, extensions and so on. -->
  3. <html lang=fr>
  4. <!-- Has to be within the first 1024 bytes, hence before the <title>
  5. See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset -->
  6. <meta charset=utf-8>
  7. <!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 -->
  8. <!-- The viewport meta is quite crowded and we are responsible for that.
  9. See: https://codepen.io/tigt/post/meta-viewport-for-2015 -->
  10. <meta name=viewport content="width=device-width,minimum-scale=1,initial-scale=1,shrink-to-fit=no">
  11. <!-- Required to make a valid HTML5 document. -->
  12. <title>About static site generators (archive) — David Larlet</title>
  13. <!-- Generated from https://realfavicongenerator.net/ such a mess. -->
  14. <link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons/apple-touch-icon.png">
  15. <link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons/favicon-32x32.png">
  16. <link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons/favicon-16x16.png">
  17. <link rel="manifest" href="/manifest.json">
  18. <link rel="mask-icon" href="/static/david/icons/safari-pinned-tab.svg" color="#5bbad5">
  19. <link rel="shortcut icon" href="/static/david/icons/favicon.ico">
  20. <meta name="apple-mobile-web-app-title" content="David Larlet">
  21. <meta name="application-name" content="David Larlet">
  22. <meta name="msapplication-TileColor" content="#da532c">
  23. <meta name="msapplication-config" content="/static/david/icons/browserconfig.xml">
  24. <meta name="theme-color" content="#f0f0ea">
  25. <!-- That good ol' feed, subscribe :p. -->
  26. <link rel=alternate type="application/atom+xml" title=Feed href="/david/log/">
  27. <meta name="robots" content="noindex, nofollow">
  28. <meta content="origin-when-cross-origin" name="referrer">
  29. <!-- Canonical URL for SEO purposes -->
  30. <link rel="canonical" href="https://fvsch.com/static-site-generators/">
  31. <style>
  32. /* http://meyerweb.com/eric/tools/css/reset/ */
  33. html, body, div, span,
  34. h1, h2, h3, h4, h5, h6, p, blockquote, pre,
  35. a, abbr, address, big, cite, code,
  36. del, dfn, em, img, ins,
  37. small, strike, strong, tt, var,
  38. dl, dt, dd, ol, ul, li,
  39. fieldset, form, label, legend,
  40. table, caption, tbody, tfoot, thead, tr, th, td,
  41. article, aside, canvas, details, embed,
  42. figure, figcaption, footer, header, hgroup,
  43. menu, nav, output, ruby, section, summary,
  44. time, mark, audio, video {
  45. margin: 0;
  46. padding: 0;
  47. border: 0;
  48. font-size: 100%;
  49. font: inherit;
  50. vertical-align: baseline;
  51. }
  52. /* HTML5 display-role reset for older browsers */
  53. article, aside, details, figcaption, figure,
  54. footer, header, hgroup, menu, nav, section { display: block; }
  55. body { line-height: 1; }
  56. blockquote, q { quotes: none; }
  57. blockquote:before, blockquote:after,
  58. q:before, q:after {
  59. content: '';
  60. content: none;
  61. }
  62. table {
  63. border-collapse: collapse;
  64. border-spacing: 0;
  65. }
  66. /* http://practicaltypography.com/equity.html */
  67. /* https://calendar.perfplanet.com/2016/no-font-face-bulletproof-syntax/ */
  68. /* https://www.filamentgroup.com/lab/js-web-fonts.html */
  69. @font-face {
  70. font-family: 'EquityTextB';
  71. src: url('/static/david/css/fonts/Equity-Text-B-Regular-webfont.woff2') format('woff2'),
  72. url('/static/david/css/fonts/Equity-Text-B-Regular-webfont.woff') format('woff');
  73. font-weight: 300;
  74. font-style: normal;
  75. font-display: swap;
  76. }
  77. @font-face {
  78. font-family: 'EquityTextB';
  79. src: url('/static/david/css/fonts/Equity-Text-B-Italic-webfont.woff2') format('woff2'),
  80. url('/static/david/css/fonts/Equity-Text-B-Italic-webfont.woff') format('woff');
  81. font-weight: 300;
  82. font-style: italic;
  83. font-display: swap;
  84. }
  85. @font-face {
  86. font-family: 'EquityTextB';
  87. src: url('/static/david/css/fonts/Equity-Text-B-Bold-webfont.woff2') format('woff2'),
  88. url('/static/david/css/fonts/Equity-Text-B-Bold-webfont.woff') format('woff');
  89. font-weight: 700;
  90. font-style: normal;
  91. font-display: swap;
  92. }
  93. @font-face {
  94. font-family: 'ConcourseT3';
  95. src: url('/static/david/css/fonts/concourse_t3_regular-webfont-20190806.woff2') format('woff2'),
  96. url('/static/david/css/fonts/concourse_t3_regular-webfont-20190806.woff') format('woff');
  97. font-weight: 300;
  98. font-style: normal;
  99. font-display: swap;
  100. }
  101. /* http://practice.typekit.com/lesson/caring-about-opentype-features/ */
  102. body {
  103. /* http://www.cssfontstack.com/ Palatino 99% Win 86% Mac */
  104. font-family: "EquityTextB", Palatino, serif;
  105. background-color: #f0f0ea;
  106. color: #07486c;
  107. font-kerning: normal;
  108. -moz-osx-font-smoothing: grayscale;
  109. -webkit-font-smoothing: subpixel-antialiased;
  110. text-rendering: optimizeLegibility;
  111. font-variant-ligatures: common-ligatures contextual;
  112. font-feature-settings: "kern", "liga", "clig", "calt";
  113. }
  114. pre, code, kbd, samp, var, tt {
  115. font-family: 'TriplicateT4c', monospace;
  116. }
  117. em {
  118. font-style: italic;
  119. color: #323a45;
  120. }
  121. strong {
  122. font-weight: bold;
  123. color: black;
  124. }
  125. nav {
  126. background-color: #323a45;
  127. color: #f0f0ea;
  128. display: flex;
  129. justify-content: space-around;
  130. padding: 1rem .5rem;
  131. }
  132. nav:last-child {
  133. border-bottom: 1vh solid #2d7474;
  134. }
  135. nav a {
  136. color: #f0f0ea;
  137. }
  138. nav abbr {
  139. border-bottom: 1px dotted white;
  140. }
  141. h1 {
  142. border-top: 1vh solid #2d7474;
  143. border-bottom: .2vh dotted #2d7474;
  144. background-color: #e3e1e1;
  145. color: #323a45;
  146. text-align: center;
  147. padding: 5rem 0 4rem 0;
  148. width: 100%;
  149. font-family: 'ConcourseT3';
  150. display: flex;
  151. flex-direction: column;
  152. }
  153. h1.single {
  154. padding-bottom: 10rem;
  155. }
  156. h1 span {
  157. position: absolute;
  158. top: 1vh;
  159. left: 20%;
  160. line-height: 0;
  161. }
  162. h1 span a {
  163. line-height: 1.7;
  164. padding: 1rem 1.2rem .6rem 1.2rem;
  165. border-radius: 0 0 6% 6%;
  166. background: #2d7474;
  167. font-size: 1.3rem;
  168. color: white;
  169. text-decoration: none;
  170. }
  171. h2 {
  172. margin: 4rem 0 1rem;
  173. border-top: .2vh solid #2d7474;
  174. padding-top: 1vh;
  175. }
  176. h3 {
  177. text-align: center;
  178. margin: 3rem 0 .75em;
  179. }
  180. hr {
  181. height: .4rem;
  182. width: .4rem;
  183. border-radius: .4rem;
  184. background: #07486c;
  185. margin: 2.5rem auto;
  186. }
  187. time {
  188. display: bloc;
  189. margin-left: 0 !important;
  190. }
  191. ul, ol {
  192. margin: 2rem;
  193. }
  194. ul {
  195. list-style-type: square;
  196. }
  197. a {
  198. text-decoration-skip-ink: auto;
  199. text-decoration-thickness: 0.05em;
  200. text-underline-offset: 0.09em;
  201. }
  202. article {
  203. max-width: 50rem;
  204. display: flex;
  205. flex-direction: column;
  206. margin: 2rem auto;
  207. }
  208. article.single {
  209. border-top: .2vh dotted #2d7474;
  210. margin: -6rem auto 1rem auto;
  211. background: #f0f0ea;
  212. padding: 2rem;
  213. }
  214. article p:last-child {
  215. margin-bottom: 1rem;
  216. }
  217. p {
  218. padding: 0 .5rem;
  219. margin-left: 3rem;
  220. }
  221. p + p,
  222. figure + p {
  223. margin-top: 2rem;
  224. }
  225. blockquote {
  226. background-color: #e3e1e1;
  227. border-left: .5vw solid #2d7474;
  228. display: flex;
  229. flex-direction: column;
  230. align-items: center;
  231. padding: 1rem;
  232. margin: 1.5rem;
  233. }
  234. blockquote cite {
  235. font-style: italic;
  236. }
  237. blockquote p {
  238. margin-left: 0;
  239. }
  240. figure {
  241. border-top: .2vh solid #2d7474;
  242. background-color: #e3e1e1;
  243. text-align: center;
  244. padding: 1.5rem 0;
  245. margin: 1rem 0 0;
  246. font-size: 1.5rem;
  247. width: 100%;
  248. }
  249. figure img {
  250. max-width: 250px;
  251. max-height: 250px;
  252. border: .5vw solid #323a45;
  253. padding: 1px;
  254. }
  255. figcaption {
  256. padding: 1rem;
  257. line-height: 1.4;
  258. }
  259. aside {
  260. display: flex;
  261. flex-direction: column;
  262. background-color: #e3e1e1;
  263. padding: 1rem 0;
  264. border-bottom: .2vh solid #07486c;
  265. }
  266. aside p {
  267. max-width: 50rem;
  268. margin: 0 auto;
  269. }
  270. /* https://fvsch.com/code/css-locks/ */
  271. p, li, pre, code, kbd, samp, var, tt, time, details, figcaption {
  272. font-size: 1rem;
  273. line-height: calc( 1.5em + 0.2 * 1rem );
  274. }
  275. h1 {
  276. font-size: 1.9rem;
  277. line-height: calc( 1.2em + 0.2 * 1rem );
  278. }
  279. h2 {
  280. font-size: 1.6rem;
  281. line-height: calc( 1.3em + 0.2 * 1rem );
  282. }
  283. h3 {
  284. font-size: 1.35rem;
  285. line-height: calc( 1.4em + 0.2 * 1rem );
  286. }
  287. @media (min-width: 20em) {
  288. /* The (100vw - 20rem) / (50 - 20) part
  289. resolves to 0-1rem, depending on the
  290. viewport width (between 20em and 50em). */
  291. p, li, pre, code, kbd, samp, var, tt, time, details, figcaption {
  292. font-size: calc( 1rem + .6 * (100vw - 20rem) / (50 - 20) );
  293. line-height: calc( 1.5em + 0.2 * (100vw - 50rem) / (20 - 50) );
  294. margin-left: 0;
  295. }
  296. h1 {
  297. font-size: calc( 1.9rem + 1.5 * (100vw - 20rem) / (50 - 20) );
  298. line-height: calc( 1.2em + 0.2 * (100vw - 50rem) / (20 - 50) );
  299. }
  300. h2 {
  301. font-size: calc( 1.5rem + 1.5 * (100vw - 20rem) / (50 - 20) );
  302. line-height: calc( 1.3em + 0.2 * (100vw - 50rem) / (20 - 50) );
  303. }
  304. h3 {
  305. font-size: calc( 1.35rem + 1.5 * (100vw - 20rem) / (50 - 20) );
  306. line-height: calc( 1.4em + 0.2 * (100vw - 50rem) / (20 - 50) );
  307. }
  308. }
  309. @media (min-width: 50em) {
  310. /* The right part of the addition *must* be a
  311. rem value. In this example we *could* change
  312. the whole declaration to font-size:2.5rem,
  313. but if our baseline value was not expressed
  314. in rem we would have to use calc. */
  315. p, li, pre, code, kbd, samp, var, tt, time, details, figcaption {
  316. font-size: calc( 1rem + .6 * 1rem );
  317. line-height: 1.5em;
  318. }
  319. p, li, pre, details {
  320. margin-left: 3rem;
  321. }
  322. h1 {
  323. font-size: calc( 1.9rem + 1.5 * 1rem );
  324. line-height: 1.2em;
  325. }
  326. h2 {
  327. font-size: calc( 1.5rem + 1.5 * 1rem );
  328. line-height: 1.3em;
  329. }
  330. h3 {
  331. font-size: calc( 1.35rem + 1.5 * 1rem );
  332. line-height: 1.4em;
  333. }
  334. figure img {
  335. max-width: 500px;
  336. max-height: 500px;
  337. }
  338. }
  339. figure.unsquared {
  340. margin-bottom: 1.5rem;
  341. }
  342. figure.unsquared img {
  343. height: inherit;
  344. }
  345. @media print {
  346. body { font-size: 100%; }
  347. a:after { content: " (" attr(href) ")"; }
  348. a, a:link, a:visited, a:after {
  349. text-decoration: underline;
  350. text-shadow: none !important;
  351. background-image: none !important;
  352. background: white;
  353. color: black;
  354. }
  355. abbr[title] { border-bottom: 0; }
  356. abbr[title]:after { content: " (" attr(title) ")"; }
  357. img { page-break-inside: avoid; }
  358. @page { margin: 2cm .5cm; }
  359. h1, h2, h3 { page-break-after: avoid; }
  360. p3 { orphans: 3; widows: 3; }
  361. img {
  362. max-width: 250px !important;
  363. max-height: 250px !important;
  364. }
  365. nav, aside { display: none; }
  366. }
  367. ul.with_columns {
  368. column-count: 1;
  369. }
  370. @media (min-width: 20em) {
  371. ul.with_columns {
  372. column-count: 2;
  373. }
  374. }
  375. @media (min-width: 50em) {
  376. ul.with_columns {
  377. column-count: 3;
  378. }
  379. }
  380. ul.with_two_columns {
  381. column-count: 1;
  382. }
  383. @media (min-width: 20em) {
  384. ul.with_two_columns {
  385. column-count: 1;
  386. }
  387. }
  388. @media (min-width: 50em) {
  389. ul.with_two_columns {
  390. column-count: 2;
  391. }
  392. }
  393. .gallery {
  394. display: flex;
  395. flex-wrap: wrap;
  396. justify-content: space-around;
  397. }
  398. .gallery figure img {
  399. margin-left: 1rem;
  400. margin-right: 1rem;
  401. }
  402. .gallery figure figcaption {
  403. font-family: 'ConcourseT3'
  404. }
  405. footer {
  406. font-family: 'ConcourseT3';
  407. display: flex;
  408. flex-direction: column;
  409. border-top: 3px solid white;
  410. padding: 4rem 0;
  411. background-color: #07486c;
  412. color: white;
  413. }
  414. footer > * {
  415. max-width: 50rem;
  416. margin: 0 auto;
  417. }
  418. footer a {
  419. color: #f1c40f;
  420. }
  421. footer .avatar {
  422. width: 200px;
  423. height: 200px;
  424. border-radius: 50%;
  425. float: left;
  426. -webkit-shape-outside: circle();
  427. shape-outside: circle();
  428. margin-right: 2rem;
  429. padding: 2px 5px 5px 2px;
  430. background: white;
  431. border-left: 1px solid #f1c40f;
  432. border-top: 1px solid #f1c40f;
  433. border-right: 5px solid #f1c40f;
  434. border-bottom: 5px solid #f1c40f;
  435. }
  436. </style>
  437. <h1>
  438. <span><a id="jumper" href="#jumpto" title="Un peu perdu ?">?</a></span>
  439. About static site generators (archive)
  440. <time>Pour la pérennité des contenus liés. Non-indexé, retrait sur simple email.</time>
  441. </h1>
  442. <section>
  443. <article>
  444. <h3><a href="https://fvsch.com/static-site-generators/">Source originale du contenu</a></h3>
  445. <p>I’ve been looking for a decent static site generator to build a simple, 10-page-or-so documentation site, and I’m failing. Here are some notes on my journey, to serve as a warning sign to future travellers, and thoughts on what static site generators could do better.</p>
  446. <h2>What is a static site generator, really?</h2>
  447. <p>I’d like to try and describe static site generators as if we hadn’t encountered this class of software yet. Please do not skip this section, even if you already (think you) know the answer.</p>
  448. <p>Static site generators (SSG) are a class of software programs that help with creating websites. They’re often written by developers — professionals or hobbyists — to power their own small website or blog, and as such they tend to have a limited feature set.</p>
  449. <p>SSG are close parents to <em>scripts</em>: short programs (a few hundred lines of code), which can be used from a command prompt to achieve specific tasks. They’re often built as a hobby by one developer, sometimes getting help from a few others months or years later. They almost never offer a graphical user interface.</p>
  450. <p>One core principle of those scripts is that they work with a source collection of plain text files (aka “content”), process those files to transform them to the HTML format, and wrap them in more HTML. This is often called a “build” step. Before running the build command, you would have a file tree that looks like this:</p>
  451. <pre><code class="language-txt">My Cool Website
  452. └── content
  453. ├── 2018-09-07-cool-blog-post.txt
  454. └── 2018-10-01-other-blog-post.txt</code></pre>
  455. <p>And after typing some text to run a command in a “terminal” or “command prompt” application, you would get something like:</p>
  456. <pre><code class="language-txt">My Cool Website
  457. ├── content
  458. │   ├── 2018-09-07-cool-blog-post.txt
  459. │   └── 2018-10-01-other-blog-post.txt
  460. └── output
  461. ├── cool-blog-post.html
  462. ├── index.html
  463. ├── other-blog-post.html
  464. └── rss.xml</code></pre>
  465. <p>With that setup, you get a simple blog with one HTML page for each blog post, an index (or home) page that lists published posts, and a RSS feed. You can write your own styles if you know CSS, or — with some SSG, but not all — use a “theme” or an example project as a starting point.</p>
  466. <p>Users of static site generators do this work on their own computer, and after the “output” folder has been generated they are expected to know how to put those files online to publish them. This can require software such as a FTP client, or more complex setups involving one or two additional command line tools.</p>
  467. <p>There are hundreds of static site generators, with many <a href="https://www.staticgen.com/">listed on StaticGen.com</a>. Some of the most popular ones are <a href="https://jekyllrb.com/">Jekyll</a>, <a href="https://gohugo.io/">Hugo</a> and <a href="https://hexo.io/">Hexo</a>.</p>
  468. <p>When it comes to editing features, there are not many to speak of. Users are expected to know what formats like <a href="https://developer.mozilla.org/en-US/docs/Learn/Getting_started_with_the_web/HTML_basics">HTML</a> and <a href="https://commonmark.org/help/">Markdown</a> are, how to prepare images themselves (in Photoshop, Gimp or similar software), how to insert images inside a page’s content by using code that may look like this:</p>
  469. <pre><code class="language-md">![My cat at 4 months](sniffles-4months.jpg)</code></pre>
  470. <p>Or even something more complex:</p>
  471. <pre><code class="language-md">![My cat at 4 months]({{&lt; imgproc sniffles-4months.jpg Resize "600x" /&gt;}})</code></pre>
  472. <p>At this point we must recognize that static site generators are tools made by developers <em>for developers</em>. They require a ton of previous knowledge: HTML, Markdown, command line interfaces, FTP or some other way to publish HTML files online, and of course you would need some kind of web hosting to put those files. Users will also need to dive into technical documentation for their tool of choice when they want to customize their site’s pages, work in “templates”, learn a templating language, and probably write some CSS.</p>
  473. <p>The most popular static site generator, Jekyll, may have the best documentation pages of the lot. And yet, it still puts a bunch of technical language front-and-center, on the project’s home page:</p>
  474. <figure class="border"><img src="jekyll-quick-start.png" alt="A series of 4 terminal commands, starting with: gem install bundler jekyll…"/><figcaption>Jekyll’s core message, “Get up and running in seconds”</figcaption></figure>
  475. <p>This is developer-talk, <em>because static site generators are tools made by developers for themselves and their peers</em>.</p>
  476. <p>On the other hand, “content management” software such as WordPress, Drupal or <a href="https://getkirby.com/">my personal favorite, Kirby</a>, target a dual audience: content editors (who write and manage content), and developers (who set up the CMS and implement the site’s design and features).</p>
  477. <p>I’m making this point because, if we own up to who our software’s audience is, then we can consider what we can do for that audience and hopefully design less terrible software.</p>
  478. <h2>A developer’s idea of elegance</h2>
  479. <p>I sometimes talk with developers who praise static site generators as <em>simple</em>, which is hogwash. I’ve seen a few projects where content editors were handed off a site based on a SSG (especially in startups where devs may dominate the conversation), and could not manage any change without a lot of training and assistance. Nothing simple about that.</p>
  480. <p>What those developers probably mean is that they find static site generators <em>elegant</em>. A programmer thinks that a solution is elegant when it uses their current knowledge to achieve something with:</p>
  481. <ul>
  482. <li>few steps,</li>
  483. <li>no repetition, and</li>
  484. <li>not too much indirection.</li>
  485. </ul>
  486. <p>Static site generators are an elegant solution for developers because:</p>
  487. <ol>
  488. <li>It rests on our previous knowledge of: file and code formats, the command line, the plain-files-and-scripts philosophy, <code>git</code> and <code>ssh</code> or <code>scp</code> and other deployment strategies, etc.</li>
  489. <li>Using this base knowledge, it offers solutions to boring, repetitive tasks. For example, Markdown syntax lets us avoid repeating <code>&lt;p&gt;…&lt;/p&gt;</code> tags in our content; templates let us avoid tasks like manually repeating HTML structure and updating menus and content lists.</li>
  490. <li>Finally, using the filesystem to store content lets us work in a single place (avoiding database software and languages, which would be overkill for limited datasets).</li>
  491. </ol>
  492. <p>I’m all for elegant solutions that fit my mindset (aka just “elegant solutions”), and I’ve used static site generators in the past, including Pelican, Hyde, Jekyll and Metalsmith.</p>
  493. <p>Here’s the thing, though: it’s been a terrible experience. It turns out that <strong>static site generators are terrible at handling content</strong>. Which is too bad, because that’s one of their very few features to begin with.</p>
  494. <p>Static site generators are elegant on principle, but are not designed to deal with content more complex than a handful of pages and a list of blog posts. And I’m not talking about the speed of building hundreds of pages or more (Jekyll is notoriously slow, Hugo is fast), but the sheer ability to <em>use content however you want</em>.</p>
  495. <h2>Static site generators are bad at content</h2>
  496. <p>Let’s back up a bit. Most static site generators were built — or at least are advertised — as alternatives to database-driven CMS software. We’ve already shown that they may only be a valid alternative for developers and technical users with a lot of prior knowledge. But they are missing another core capability of CMS software.</p>
  497. <p>In your average PHP or Python CMS, as in many Web frameworks, querying content from a relational database is always possible (and more or less straightforward). Linked objects and/or hierarchical content structures are built in features. Building home pages, landing pages, lists, modular pages, product pages with many sections, etc., out of content hierarchies or relations is standard practice; CMS software that was too limited to do that either evolved or was replaced by their competition.</p>
  498. <p>When I’ve used static site generators in the past ten years, there were a few pain points like lacking documentation and strange and incompatible conventions. But the nail in the coffin was always that it’s either impossible or way too hard to <strong>build a single page from several pieces of content</strong>.</p>
  499. <p>Recently, I’ve looked at static site generators again, because I wanted to convert Kaliop’s <a href="https://kaliop.github.io/frontend-style-guide/2.0/">Frontend Style Guide</a>, originally built with Kirby CMS, to something that could be built to static HTML in fewer steps <em>by developers</em> (e.g. <code>npm install &amp;&amp; npm run build</code>).</p>
  500. <p>This site is made of a handful of long “chapter” pages, with a top-level table of contents which lists all chapters and their sections:</p>
  501. <figure class="border"><img alt="Screenshot showing a full table of contents with 3 levels of titles" src="style-guide.png" srcset="style-guide.png 1x, style-guide@2x.png 2x"/></figure>
  502. <p>For easier <em>content management</em> and reordering of sections, I had broken each chapter into different section files, so that the index page is generated from three levels of content. Looking at the “General Rules” branch only, the content hierarchy looks like:</p>
  503. <pre><code class="language-txt">2.0
  504. └─ general
  505. ├─ 1-readme
  506. ├─ 2-editorconfig
  507. ├─ 3-dry
  508. └─ 4-clean</code></pre>
  509. <p>When generating the full table of contents, I expect to be able to write (in pseudo-code):</p>
  510. <pre><code class="language-txt">for chapter in page.children:
  511. print chapter.title
  512. for section in chapter.children:
  513. print section.title</code></pre>
  514. <p>Similarly, when generating a chapter page, I expect that I can easily print the full chapter content:</p>
  515. <pre><code class="language-txt">for section in page.children:
  516. print section.title
  517. print section.content</code></pre>
  518. <p>Actual templates would be a bit more complex, since we need to wrap this content in HTML tags, and maybe pass it to template partials or components. But retrieving content from a hierarchy of local files should be straightforward.</p>
  519. <p><strong>I haven’t found a single static site generator that handles this use case gracefully.</strong> Instead, after reviewing the documentation of three dozens tools (listed on <a href="https://www.staticgen.com/">StaticGen</a>), and actively trying to build my use case with a handful of them, I’ve seen a range of limitations and awkward workarounds:</p>
  520. <ol>
  521. <li>Sometimes it’s just not possible to work with content hierarchies and partial content. (Most JavaScript static site generators fall into that category.)</li>
  522. <li>Or you have to write some configuration code that populates “Collections” ahead of build time. (Too indirect, and breaks if you change your information architecture a bit. We need something a bit closer to direct manipulation!)</li>
  523. <li>Finally, a short list of tools allow listing a page’s children or “resources”, but have bugs when dealing with more than one level (<a href="https://www.getgutenberg.io/">Gutenberg</a>) or impose arbitrary restrictions on what page “types” can list different types of content (<a href="https://gohugo.io/">Hugo</a>).</li>
  524. </ol>
  525. <p>When you <em>can</em> list content from children, grandchildren or other locations, it’s often not possible to <em>process</em> this content as Markdown or access its metadata.</p>
  526. <h2>A few words of warning about Hugo</h2>
  527. <p>The <a href="https://gohugo.io/">Hugo</a> static site generator, praised by many for its speed, is more capable than most. There are surely many good things to say about Hugo, but in the spirit of warning folks about the terribleness of software: I wouldn’t recommend it for anyone who wants a smooth learning experience.</p>
  528. <p>For one thing, it requires learning about way too many specific concepts: Sections, Lists, Taxonomies, Page Bundles, Leaf Bundles, Template lookup order, and the perfectly unintuitive differences between a <code>index.md</code> page and a <code>_index.md</code> page. (Or are those Sections, or Bundles? Is one a Section and the other a Page? Are Sections and Bundles “pages” too? I have no idea.)</p>
  529. <p>I particularly dislike the way Hugo <a href="https://gohugo.io/templates/lookup-order/">matches a page to a dozen possible template files</a> or more — taking a page from Drupal’s book of idiosyncratic awfulness. Now, I understand that this is meant to make it possible to use different “themes”, and at worst your pages will still render with the theme’s <code>layouts/_default/list.html</code> or <code>layouts/_default/single.html</code> templates. But if you want to write your own HTML and CSS, the template lookup stuff is a nuisance (much like it is in the Drupal world when you want any kind of control over the HTML output).</p>
  530. <p>On top of the complexity of Hugo’s design, its documentation is often subpar. For instance, the <a href="https://gohugo.io/content-management/page-resources/">“Content Management &gt; Page Resources” page</a> does not explain much about, well, content, or content management. I expected a description showing content examples (some markdown or a file structure maybe), then information about how to use it in templates and other contexts. Instead, it’s a kind of API documentation listing “Properties” and “Methods”.</p>
  531. <p>The second issue is that while Hugo builds pages <em>fast</em>, it often <em>builds the wrong thing fast</em>. For instance, if I rename a <code>index.md</code> file to <code>_index.md</code>, I need to run the <code>hugo</code> command two or three times to get it to render with the correct template. Using <code>hugo server</code> seems even less reliable.</p>
  532. <p>Hugo is also unhelpful when a source Markdown file doesn’t render anything (there is no log or warning) or outputs a cryptic <code>&lt;pre&gt;&lt;/pre&gt;</code>. This doesn’t help the learning curve.</p>
  533. <p>That being said, I heard that Hugo is an interesting choice if you’re building hundreds or thousands of pages (because it builds these pages really quickly), in which case it might be worth spending a couple weeks learning to use it.</p>
  534. <h2>What could we do differently?</h2>
  535. <p>There are thousands of static site generators out there (counting a bunch of scripts with 5 stars on GitHub) because thousands of developers looked at existing solutions and said “eh, I’ll just build my own”.</p>
  536. <p>As a thought experiment, here are the core topics I would look at if I wanted to build my own tool from scratch.</p>
  537. <h3>1. Content naming conventions</h3>
  538. <p>Conventions for naming content files are necessary to allow users to create content files quickly and get predictable results. Those conventions must be short, clearly defined, clearly explained (you need great docs here), and intuitive (when possible, follow existing conventions rather than roll your own).</p>
  539. <p>Major issues to solve regarding source content:</p>
  540. <ul>
  541. <li>What will translate to a web page?</li>
  542. <li>What will be ignored?</li>
  543. <li>How may users control the transformations that are applied to content (processing of Markdown or other formats, templates, image transformations)? Should this be defined in the content or elsewhere?</li>
  544. <li>How do you generate alternative views for the same page, e.g. a HTML page and a RSS or Atom feed?</li>
  545. <li>How can you generate category and tag pages? More generally, how do you generate any limited set of pages based on querying information from other content, without having to manually create a new (probably empty) content file?</li>
  546. </ul>
  547. <p>Some of those concerns can be addressed in templates or in configuration or other code, but there’s value in using elegant conventions to let users directly manipulate or tweak content to get a desired result.</p>
  548. <h3>2. Give feedback on content-to-output mapping</h3>
  549. <p>The generator should provide, by default, feedback on which files are picked up, which are ignored, and why. When a file is picked up and processed in some way, it should say so too.</p>
  550. <p>To surface this information efficiently, the generator may need to come with a GUI (a web UI could work). I would be looking at tools like <a href="https://fractal.build/">Fractal</a> for inspiration.</p>
  551. <h3>3. More code should happen in templates</h3>
  552. <p>Most generators use restrictive template engines, and feed a restricted context (aka a set of data and functions) to those templates. This seems to come from a misguided need to keep templates “simple”, “logic-less”, and other kinds of baloney. Look, in a big MVC application built by people with different roles and technical knowledge, it might be useful to have your very responsible backend developers write controllers and let designers or frontend developers write HTML in a sandbox. Well, that’s a questionable approach too.</p>
  553. <p>Anyway, as we said we’re making a tool for technically-minded people, and should give them power to actually do stuff. And since we’re talking about websites built ahead-of-time, the risks of logic in templates don’t apply as much.</p>
  554. <p>Another consequence of restricted templates is that SSG documentation will then tell you to work in a third place: not in content, not in templates, but in “config code”. This is a problem because A) it’s probably one place too many and B) it’s often unclear at what time this config code is running or how often: once for every page generation or template run, or just once at the start of a build?</p>
  555. <p>So, do more work in templates. We’re making somewhat simple websites here, it’s going to be okay. It’s cool if you can refactor your code to avoid duplication or separate concerns or whatever, but this shouldn’t be a requirement; users will only jump through hoops if they’re working on a big enough project to justify it.</p>
  556. <h3>4. A full content API</h3>
  557. <p>There should be a full content traversal API, which could be modelled after <a href="https://getkirby.com/docs/templates/api">Kirby’s API</a> (itself inspired by jQuery). Other sources of inspiration:</p>
  558. <ul>
  559. <li>Globbing, e.g. <code>find('posts/*.md')</code>.</li>
  560. <li>DOM traversal (there’s no reason why your content tree cannot be a tree of nodes).</li>
  561. </ul>
  562. <p>One concern with offering a full content API in templates is that if you run a template for a thousand pages, and do the same work (including querying the filesystem) every time, you’re going to have performance issues. I have a few possible optimizations in mind (fragmenting content queries and memoization), but I’ll admit that as a UI developer this is not my forte.</p>
  563. <p>On top of traversing and retrieving content, templates should be able to transform formats (Markdown to HTML, parsing JSON and maybe YAML) and process images.</p>
  564. <h3>5. Markdown plus front-matter is too limiting</h3>
  565. <p>Originating in simple blog engines, static site generators treat pages as a single content chunk (often in Markdown) with some metadata sprinkled on top. If you need several long chunks of content (say, a product short description, long description, technical specs, and a list of vendors), you’re out of luck.</p>
  566. <p>Theoretically, you can put any kind of text content in YAML “front-matter”, but editing that content without breaking the YAML syntax is a pain.</p>
  567. <p>One workaround is using separate files in the same folder or in subfolder, than use the content API to retrieve them. Each such “resource” can have a body and metadata.</p>
  568. <p>Kirby does things a bit differently (though it’s always possible to use a page’s files or child pages too), using a custom format with arbitrary fields:</p>
  569. <pre><code>Title: My Title
  570. ----
  571. Date: 2018-12-25
  572. ----
  573. Desc: Short description for list views and SEO.
  574. ----
  575. Body:
  576. ## A field contains arbitrary text
  577. So we can use a field value and parse it as Markdown or whatever.
  578. ----
  579. Notes: …
  580. ----
  581. References: …</code></pre>
  582. <p>Other sources of inspiration:</p>
  583. <ul>
  584. <li>Vue single-file components, and, well, XML</li>
  585. <li>Inside the page’s body: shortcodes (WordPress, Hugo), Web Components, Vue components (Vuepress)</li>
  586. <li>Separators using HTML comment syntax, like <code>&lt;!--more--&gt;</code></li>
  587. </ul>
  588. <p>I’m not sure what would be the “best” solution, but it should be possible to unlock more power without completely breaking with convention.</p>
  589. <h3>6. No theming system</h3>
  590. <p>A theming infrastructure imposes a lot of technical restrictions and indirection, such as template lookup orders (see: Hugo), having to specify a handful of default template names (Hugo, again), and having to specify strange content conventions for mapping content to those default template names (still Hugo). Frankly, if a user really wants a template inheritance and theme inheritance mechanism, they’ll probably bite the bullet and work with WordPress or Drupal.</p>
  591. <p>In the lightweight CMS world, having a theming system is the main reason why <a href="https://getgrav.org/">Grav</a> is less straightforward than <a href="https://getkirby.com/">Kirby</a> (which inspired it): themes and plugins rely on metadata in pages, so you have to put detailed config-as-metadata in every page to enable and configure theme and plugin features, which often feels like randomly pressing buttons and hoping to get a result.</p>
  592. </article>
  593. </section>
  594. <nav id="jumpto">
  595. <p>
  596. <a href="/david/blog/">Accueil du blog</a> |
  597. <a href="https://fvsch.com/static-site-generators/">Source originale</a> |
  598. <a href="/david/stream/2019/">Accueil du flux</a>
  599. </p>
  600. </nav>
  601. <footer>
  602. <div>
  603. <img src="/static/david/david-larlet-avatar.jpg" loading="lazy" class="avatar" width="200" height="200">
  604. <p>
  605. Bonjour/Hi!
  606. Je suis <a href="/david/" title="Profil public">David&nbsp;Larlet</a>, je vis actuellement à Montréal et j’alimente cet espace depuis 15 ans. <br>
  607. Si tu as apprécié cette lecture, n’hésite pas à poursuivre ton exploration. Par exemple via les <a href="/david/blog/" title="Expériences bienveillantes">réflexions bimestrielles</a>, la <a href="/david/stream/2019/" title="Pensées (dés)articulées">veille hebdomadaire</a> ou en t’abonnant au <a href="/david/log/" title="S’abonner aux publications via RSS">flux RSS</a> (<a href="/david/blog/2019/flux-rss/" title="Tiens c’est quoi un flux RSS ?">so 2005</a>).
  608. </p>
  609. <p>
  610. Je m’intéresse à la place que je peux avoir dans ce monde. En tant qu’humain, en tant que membre d’une famille et en tant qu’associé d’une coopérative. De temps en temps, je fais aussi des <a href="https://github.com/davidbgk" title="Principalement sur Github mais aussi ailleurs">trucs techniques</a>. Et encore plus rarement, <a href="/david/talks/" title="En ce moment je laisse plutôt la place aux autres">j’en parle</a>.
  611. </p>
  612. <p>
  613. Voici quelques articles choisis :
  614. <a href="/david/blog/2019/faire-equipe/" title="Accéder à l’article complet">Faire équipe</a>,
  615. <a href="/david/blog/2018/bivouac-automnal/" title="Accéder à l’article complet">Bivouac automnal</a>,
  616. <a href="/david/blog/2018/commodite-effondrement/" title="Accéder à l’article complet">Commodité et effondrement</a>,
  617. <a href="/david/blog/2017/donnees-communs/" title="Accéder à l’article complet">Des données aux communs</a>,
  618. <a href="/david/blog/2016/accompagner-enfant/" title="Accéder à l’article complet">Accompagner un enfant</a>,
  619. <a href="/david/blog/2016/senior-developer/" title="Accéder à l’article complet">Senior developer</a>,
  620. <a href="/david/blog/2016/illusion-sociale/" title="Accéder à l’article complet">L’illusion sociale</a>,
  621. <a href="/david/blog/2016/instantane-scopyleft/" title="Accéder à l’article complet">Instantané Scopyleft</a>,
  622. <a href="/david/blog/2016/enseigner-web/" title="Accéder à l’article complet">Enseigner le Web</a>,
  623. <a href="/david/blog/2016/simplicite-defaut/" title="Accéder à l’article complet">Simplicité par défaut</a>,
  624. <a href="/david/blog/2016/minimalisme-esthetique/" title="Accéder à l’article complet">Minimalisme et esthétique</a>,
  625. <a href="/david/blog/2014/un-web-omni-present/" title="Accéder à l’article complet">Un web omni-présent</a>,
  626. <a href="/david/blog/2014/manifeste-developpeur/" title="Accéder à l’article complet">Manifeste de développeur</a>,
  627. <a href="/david/blog/2013/confort-convivialite/" title="Accéder à l’article complet">Confort et convivialité</a>,
  628. <a href="/david/blog/2013/testament-numerique/" title="Accéder à l’article complet">Testament numérique</a>,
  629. et <a href="/david/blog/" title="Accéder aux archives">bien d’autres…</a>
  630. </p>
  631. <p>
  632. On peut <a href="mailto:david%40larlet.fr" title="Envoyer un courriel">échanger par courriel</a>. Si éventuellement tu souhaites que l’on travaille ensemble, tu devrais commencer par consulter le <a href="http://larlet.com">profil dédié à mon activité professionnelle</a> et/ou contacter directement <a href="http://scopyleft.fr/">scopyleft</a>, la <abbr title="Société coopérative et participative">SCOP</abbr> dont je fais partie depuis six ans. Je recommande au préalable de lire <a href="/david/blog/2018/cout-site/" title="Attention ce qui va suivre peut vous choquer">combien coûte un site</a> et pourquoi je suis plutôt favorable à une <a href="/david/pro/devis/" title="Discutons-en !">non-demande de devis</a>.
  633. </p>
  634. <p>
  635. Je ne traque pas ta navigation mais mon
  636. <abbr title="Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33.184162340">hébergeur</abbr>
  637. conserve des logs d’accès.
  638. </p>
  639. </div>
  640. </footer>
  641. <script type="text/javascript">
  642. ;(_ => {
  643. const jumper = document.getElementById('jumper')
  644. jumper.addEventListener('click', e => {
  645. e.preventDefault()
  646. const anchor = e.target.getAttribute('href')
  647. const targetEl = document.getElementById(anchor.substring(1))
  648. targetEl.scrollIntoView({behavior: 'smooth'})
  649. })
  650. })()
  651. </script>