├── .eleventy.js ├── .eleventyignore ├── .github ├── actions │ └── setup │ │ ├── Dockerfile │ │ └── entrypoint.sh └── workflows │ └── deploy-to-github-pages.yml ├── .gitignore ├── 404.md ├── README.md ├── _11ty └── getTagList.js ├── _data └── metadata.json ├── _includes ├── base.njk ├── home.njk ├── post.njk └── postslist.njk ├── about └── index.md ├── css ├── index.css └── prism-base16-monokai.dark.css ├── feed ├── feed.njk └── htaccess.njk ├── gulpfile.js ├── img └── .gitkeep ├── index.njk ├── package-lock.json ├── package.json ├── posts └── .gitkeep ├── sitemap.xml.njk ├── tags.njk └── tagslist.njk /.eleventy.js: -------------------------------------------------------------------------------- 1 | const { DateTime } = require("luxon"); 2 | const fs = require("fs"); 3 | const pluginRss = require("@11ty/eleventy-plugin-rss"); 4 | const pluginSyntaxHighlight = require("@11ty/eleventy-plugin-syntaxhighlight"); 5 | 6 | module.exports = function(eleventyConfig) { 7 | eleventyConfig.addPlugin(pluginRss); 8 | eleventyConfig.addPlugin(pluginSyntaxHighlight); 9 | eleventyConfig.setDataDeepMerge(true); 10 | 11 | eleventyConfig.addLayoutAlias("post", "layouts/post.njk"); 12 | 13 | eleventyConfig.addFilter("readableDate", dateObj => { 14 | return DateTime.fromJSDate(dateObj, { zone: "utc" }).toFormat( 15 | "dd LLL yyyy" 16 | ); 17 | }); 18 | 19 | // https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-date-string 20 | eleventyConfig.addFilter("htmlDateString", dateObj => { 21 | return DateTime.fromJSDate(dateObj, { zone: "utc" }).toFormat("yyyy-LL-dd"); 22 | }); 23 | 24 | // Get the first `n` elements of a collection. 25 | eleventyConfig.addFilter("head", (array, n) => { 26 | if (n < 0) { 27 | return array.slice(n); 28 | } 29 | 30 | return array.slice(0, n); 31 | }); 32 | 33 | eleventyConfig.addCollection("tagList", require("./_11ty/getTagList")); 34 | 35 | eleventyConfig.addPassthroughCopy("img"); 36 | eleventyConfig.addPassthroughCopy("css"); 37 | 38 | /* Markdown Plugins */ 39 | let markdownIt = require("markdown-it"); 40 | let markdownItAnchor = require("markdown-it-anchor"); 41 | let options = { 42 | html: true, 43 | breaks: true, 44 | linkify: true 45 | }; 46 | let opts = { 47 | permalink: true, 48 | permalinkClass: "direct-link", 49 | permalinkSymbol: "#" 50 | }; 51 | 52 | eleventyConfig.setLibrary( 53 | "md", 54 | markdownIt(options).use(markdownItAnchor, opts) 55 | ); 56 | 57 | eleventyConfig.setBrowserSyncConfig({ 58 | callbacks: { 59 | ready: function(err, browserSync) { 60 | const content_404 = fs.readFileSync("_site/404.html"); 61 | 62 | browserSync.addMiddleware("*", (req, res) => { 63 | // Provides the 404 content without redirect. 64 | res.write(content_404); 65 | res.end(); 66 | }); 67 | } 68 | } 69 | }); 70 | 71 | return { 72 | templateFormats: ["md", "njk", "html", "liquid"], 73 | 74 | // If your site lives in a different subdirectory, change this. 75 | // Leading or trailing slashes are all normalized away, so don’t worry about it. 76 | // If you don’t have a subdirectory, use "" or "/" (they do the same thing) 77 | // This is only used for URLs (it does not affect your file structure) 78 | pathPrefix: "/blog", 79 | 80 | markdownTemplateEngine: "liquid", 81 | htmlTemplateEngine: "njk", 82 | dataTemplateEngine: "njk", 83 | passthroughFileCopy: true, 84 | dir: { 85 | input: ".", 86 | includes: "_includes", 87 | data: "_data", 88 | output: "_site" 89 | } 90 | }; 91 | }; 92 | -------------------------------------------------------------------------------- /.eleventyignore: -------------------------------------------------------------------------------- 1 | README.md 2 | _11ty/ 3 | -------------------------------------------------------------------------------- /.github/actions/setup/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12-alpine 2 | 3 | COPY entrypoint.sh /entrypoint.sh 4 | ENTRYPOINT ["/entrypoint.sh"] 5 | CMD ["node"] 6 | -------------------------------------------------------------------------------- /.github/actions/setup/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | # eval $(ssh-agent -s) 6 | # echo -e "StrictHostKeyChecking no" >> /etc/ssh/ssh_config 7 | # mkdir -p ~/.ssh 8 | # printf "$GH_SSH_KEY" > ~/.ssh/id_rsa 9 | # chmod 600 ~/.ssh/id_rsa 10 | # ssh-add ~/.ssh/id_rsa 11 | # ssh-add -l 12 | 13 | npm ci 14 | sh -c "$*" 15 | -------------------------------------------------------------------------------- /.github/workflows/deploy-to-github-pages.yml: -------------------------------------------------------------------------------- 1 | on: [issues, push] 2 | name: Deploy to GitHub Pages 3 | jobs: 4 | build: 5 | name: Run deploy.sh for embed apps 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@master 9 | - name: Get posts from GitHub Issues 10 | uses: ./.github/actions/setup 11 | env: 12 | GH_API_TOKEN: ${{ secrets.GH_API_TOKEN }} 13 | with: 14 | args: npx gulp github:issues 15 | - name: Deploy to GitHub Pages 16 | uses: JamesIves/github-pages-deploy-action@master 17 | env: 18 | ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} 19 | BRANCH: gh-pages 20 | BUILD_SCRIPT: npm ci && npm run build 21 | FOLDER: _site 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | _site 3 | node_modules 4 | -------------------------------------------------------------------------------- /404.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home.njk 3 | permalink: 404.html 4 | eleventyExcludeFromCollections: true 5 | --- 6 |

Content not found.

7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @pioug/blog 2 | 3 | - Static site generated with [11ty](https://www.11ty.io/) 4 | - Content authored with [GitHub Issues](https://github.com/pioug/blog/issues) 5 | - Assets hosted with [GitHub Pages](https://pages.github.com/) 6 | - Assets deployed with [GitHub Actions](https://github.com/features/actions) 7 | 8 | ## Setup 9 | 10 | **GitHub Actions is required** (still in beta as far as know). 11 | 12 | 1. Fork (or copy) the repository. 13 | 2. Store GitHub access token as [Secrets](https://developer.github.com/actions/managing-workflows/storing-secrets/): 14 | 15 | - `GH_API_TOKEN`: See [Creating a personal access token](https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line#creating-a-token). 16 | - `ACCESS_TOKEN`: Same as above but since I reuse an [existing action](https://github.com/JamesIves/github-pages-deploy-action), the environment variable has a different name than mine. 17 | 18 | 3. Enable GitHub Pages in the repository settings and set the `gh-pages` branch as source. 19 | 20 | ## Workflow 21 | 22 | 1. Create (or edit) an issue in the GitHub repository. 23 | 2. GitHub Actions receives an `issues` event. 24 | 3. A workflow fetches all issues labelled as `posts` in the repository using the GraphQL API of GitHub then uses Eleventy to compile the Markdown files using the body of the issues. The front matter is built from the title, tags and creation date of the issues. 25 | 4. Another workflow git-commits and git-pushes the build folder to the GitHub Pages branch. 26 | 5. GitHub Pages assets are automatically refreshed. 27 | 28 | ## Why 29 | 30 | - Reuse the label system to manage the post tags (easy to assign, rename on GitHub). 31 | - Articles are updated automatically as soon as I finish editing the corresponding issues. 32 | - Image upload is simple as copy-pasting an image on GitHub 33 | - No web server, no file bucket, no CDN to manage. 34 | - The articles are editable in the browser. 35 | - Bonus: No vendor lock-in? I feel like I could easily change the Docker/scripting part (used by GH Actions) or the hosting (S3 + CF) or the static site generator or without too much trouble. I can always return to markdown files if GitHub issues are a bad idea. 36 | - Bonus: Giving editing permissions could be as simple as giving someone access 37 | -------------------------------------------------------------------------------- /_11ty/getTagList.js: -------------------------------------------------------------------------------- 1 | module.exports = function(collection) { 2 | let tagSet = new Set(); 3 | collection.getAll().forEach(function(item) { 4 | if( "tags" in item.data ) { 5 | let tags = item.data.tags; 6 | 7 | tags = tags.filter(function(item) { 8 | switch(item) { 9 | // this list should match the `filter` list in tags.njk 10 | case "all": 11 | case "nav": 12 | case "post": 13 | case "posts": 14 | return false; 15 | } 16 | 17 | return true; 18 | }); 19 | 20 | for (const tag of tags) { 21 | tagSet.add(tag); 22 | } 23 | } 24 | }); 25 | 26 | // returning an array in addCollection works in Eleventy 0.5.3 27 | return [...tagSet]; 28 | }; 29 | -------------------------------------------------------------------------------- /_data/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "@pioug/blog", 3 | "url": "https://pioug.github.io/", 4 | "description": "I am writing about my experiences as a naval navel-gazer.", 5 | "feed": { 6 | "subtitle": "I am writing about my experiences as a naval navel-gazer.", 7 | "filename": "feed.xml", 8 | "path": "/feed/feed.xml", 9 | "url": "https://pioug.github.io/feed/feed.xml", 10 | "id": "https://pioug.github.io/" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /_includes/base.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ renderData.title or title or metadata.title }} 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |

{{ metadata.title }}

15 |
16 | 17 |
18 | {{ content | safe }} 19 |
20 | 21 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /_includes/home.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: base.njk 3 | --- 4 | 5 | {{ content | safe }} 6 | -------------------------------------------------------------------------------- /_includes/post.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: base.njk 3 | --- 4 |

{{ title }}

5 | {% if date %} 6 |

7 | {% endif %} 8 |
9 | {% for tag in tags %} 10 | {%- if tag != "posts" -%} 11 | {%- if tag != "nav" -%} 12 | {% set tagUrl %}/tags/{{ tag }}/{% endset %} 13 | #{{ tag }} 14 | {%- endif -%} 15 | {%- endif -%} 16 | {% endfor %} 17 |
18 | 19 | {{ content | safe }} 20 | -------------------------------------------------------------------------------- /_includes/postslist.njk: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /about/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post.njk 3 | title: About 4 | tags: 5 | - nav 6 | navtitle: About 7 | --- 8 | 9 | I am a person. 10 | -------------------------------------------------------------------------------- /css/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, 7 | sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol; 8 | line-height: 1.4; 9 | max-width: 800px; 10 | padding: 0 1rem; 11 | margin: 1rem auto; 12 | } 13 | 14 | a { 15 | color: DodgerBlue; 16 | text-decoration: none; 17 | } 18 | 19 | main { 20 | margin: 3rem 0; 21 | } 22 | 23 | .nav-item-active a { 24 | color: MidnightBlue; 25 | } 26 | 27 | .tag { 28 | color: DeepPink; 29 | } 30 | 31 | time { 32 | font-weight: normal; 33 | } 34 | 35 | p img { 36 | display: block; 37 | margin: auto; 38 | max-width: 100%; 39 | } 40 | 41 | nav h2, 42 | .title-page { 43 | font-variant: small-caps; 44 | font-weight: normal; 45 | } 46 | -------------------------------------------------------------------------------- /css/prism-base16-monokai.dark.css: -------------------------------------------------------------------------------- 1 | pre, 2 | code { 3 | font-family: Consolas, Menlo, Monaco, "Andale Mono WT", "Andale Mono", 4 | "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", 5 | "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", 6 | "Courier New", Courier, monospace; 7 | line-height: 1.5; 8 | } 9 | pre { 10 | font-size: 14px; 11 | line-height: 1.375; 12 | direction: ltr; 13 | text-align: left; 14 | white-space: pre; 15 | word-spacing: normal; 16 | word-break: normal; 17 | -moz-tab-size: 2; 18 | -o-tab-size: 2; 19 | tab-size: 2; 20 | -webkit-hyphens: none; 21 | -moz-hyphens: none; 22 | -ms-hyphens: none; 23 | hyphens: none; 24 | padding: 1em; 25 | margin: 0.5em 0; 26 | background-color: #f6f6f6; 27 | } 28 | .highlight-line { 29 | display: block; 30 | padding: 0.125em 1em; 31 | text-decoration: none; /* override del, ins, mark defaults */ 32 | color: inherit; /* override del, ins, mark defaults */ 33 | } 34 | 35 | /* allow highlighting empty lines */ 36 | .highlight-line:empty:before { 37 | content: " "; 38 | } 39 | /* avoid double line breaks when using display: block; */ 40 | .highlight-line + br { 41 | display: none; 42 | } 43 | 44 | .highlight-line-isdir { 45 | color: #b0b0b0; 46 | background-color: #222; 47 | } 48 | .highlight-line-active { 49 | background-color: #444; 50 | background-color: hsla(0, 0%, 27%, 0.8); 51 | } 52 | .highlight-line-add { 53 | background-color: #45844b; 54 | } 55 | .highlight-line-remove { 56 | background-color: #902f2f; 57 | } 58 | code[class*="language-"], 59 | pre[class*="language-"] { 60 | font-size: 14px; 61 | line-height: 1.375; 62 | direction: ltr; 63 | text-align: left; 64 | white-space: pre; 65 | word-spacing: normal; 66 | word-break: normal; 67 | -moz-tab-size: 2; 68 | -o-tab-size: 2; 69 | tab-size: 2; 70 | -webkit-hyphens: none; 71 | -moz-hyphens: none; 72 | -ms-hyphens: none; 73 | hyphens: none; 74 | background: #272822; 75 | color: #f8f8f2; 76 | } 77 | pre[class*="language-"] { 78 | padding: 1.5em 0; 79 | margin: 0.5em 0; 80 | overflow: auto; 81 | } 82 | :not(pre) > code[class*="language-"] { 83 | padding: 0.1em; 84 | border-radius: 0.3em; 85 | } 86 | .token.comment, 87 | .token.prolog, 88 | .token.doctype, 89 | .token.cdata { 90 | color: #75715e; 91 | } 92 | .token.punctuation { 93 | color: #f8f8f2; 94 | } 95 | .token.namespace { 96 | opacity: 0.7; 97 | } 98 | .token.operator, 99 | .token.boolean, 100 | .token.number { 101 | color: #fd971f; 102 | } 103 | .token.property { 104 | color: #f4bf75; 105 | } 106 | .token.tag { 107 | color: #66d9ef; 108 | } 109 | .token.string { 110 | color: #a1efe4; 111 | } 112 | .token.selector { 113 | color: #ae81ff; 114 | } 115 | .token.attr-name { 116 | color: #fd971f; 117 | } 118 | .token.entity, 119 | .token.url, 120 | .language-css .token.string, 121 | .style .token.string { 122 | color: #a1efe4; 123 | } 124 | .token.attr-value, 125 | .token.keyword, 126 | .token.control, 127 | .token.directive, 128 | .token.unit { 129 | color: #a6e22e; 130 | } 131 | .token.statement, 132 | .token.regex, 133 | .token.atrule { 134 | color: #a1efe4; 135 | } 136 | .token.placeholder, 137 | .token.variable { 138 | color: #66d9ef; 139 | } 140 | .token.deleted { 141 | text-decoration: line-through; 142 | } 143 | .token.inserted { 144 | border-bottom: 1px dotted #f9f8f5; 145 | text-decoration: none; 146 | } 147 | .token.italic { 148 | font-style: italic; 149 | } 150 | .token.important, 151 | .token.bold { 152 | font-weight: bold; 153 | } 154 | .token.important { 155 | color: #f92672; 156 | } 157 | .token.entity { 158 | cursor: help; 159 | } 160 | pre > code.highlight { 161 | outline: 0.4em solid #f92672; 162 | outline-offset: 0.4em; 163 | } 164 | -------------------------------------------------------------------------------- /feed/feed.njk: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: feed/feed.xml 3 | eleventyExcludeFromCollections: true 4 | --- 5 | 6 | 7 | {{ metadata.title }} 8 | {{ metadata.feed.subtitle }} 9 | 10 | 11 | {{ collections.posts | rssLastUpdatedDate }} 12 | {{ metadata.feed.id }} 13 | 14 | {{ metadata.author.name }} 15 | {{ metadata.author.email }} 16 | 17 | {%- for post in collections.posts %} 18 | {% set absolutePostUrl %}{{ post.url | url | absoluteUrl(metadata.url) }}{% endset %} 19 | 20 | {{ post.data.title }} 21 | 22 | {{ post.date | rssDate }} 23 | {{ absolutePostUrl }} 24 | {{ post.templateContent | htmlToAbsoluteUrls(absolutePostUrl) }} 25 | 26 | {%- endfor %} 27 | 28 | -------------------------------------------------------------------------------- /feed/htaccess.njk: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: feed/.htaccess 3 | eleventyExcludeFromCollections: true 4 | --- 5 | # For Apache, to show `{{ metadata.feed.filename }}` when browsing to directory /feed/ (hide the file!) 6 | DirectoryIndex {{ metadata.feed.filename }} 7 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const gulp = require("gulp"); 4 | 5 | const GH_API_TOKEN = process.env.GH_API_TOKEN; 6 | 7 | gulp.task("github:issues", getGitHubIssues); 8 | 9 | async function getGitHubIssues() { 10 | const graphql = require("graphql.js"); 11 | const graph = graphql("https://api.github.com/graphql", { 12 | headers: { 13 | Authorization: `Bearer ${GH_API_TOKEN}`, 14 | "User-Agent": "pioug" 15 | }, 16 | asJSON: true 17 | }); 18 | const query = graph( 19 | ` 20 | query ($number_of_issues: Int!) { 21 | repository(owner:"pioug", name:"blog") { 22 | issues(last:20) { 23 | edges { 24 | node { 25 | title 26 | createdAt, 27 | body, 28 | bodyText, 29 | labels(first: $number_of_issues) { 30 | edges { 31 | node { 32 | name 33 | } 34 | } 35 | } 36 | } 37 | } 38 | } 39 | } 40 | } 41 | ` 42 | ); 43 | const { 44 | repository: { 45 | issues: { edges: issues } 46 | } 47 | } = await query({ 48 | number_of_issues: 100 49 | }); 50 | 51 | issues 52 | .filter(({ node: { labels: { edges: labels } } }) => 53 | labels.some(({ name }) => "posts") 54 | ) 55 | .forEach(function({ node: issue }) { 56 | const fs = require("fs"); 57 | const slugify = require("slugify"); 58 | const { title, createdAt, body, bodyText, labels } = issue; 59 | const description = bodyText 60 | .replace(/\s+/g, " ") 61 | .split(" ") 62 | .reduce( 63 | (res, token) => (res.length > 200 ? `${res}` : res + " " + token), 64 | "" 65 | ); 66 | const md = [ 67 | `---`, 68 | `title: ${title}`, 69 | `date: ${createdAt.toString()}`, 70 | `description: ${ 71 | description.length >= 200 ? description + "..." : description 72 | }`, 73 | `layout: post.njk`, 74 | `tags:`, 75 | `${labels.edges.map(label => ` - ${label.node.name}`).join("\n")}`, 76 | `---`, 77 | `${body}` 78 | ].join("\n"); 79 | const filename = 80 | slugify(title, { 81 | remove: /[.,\/#!$%\^&\*;:{}=\-_`~()]/g, 82 | lower: true 83 | }) + ".md"; 84 | console.log(`Write ${filename}`); 85 | fs.writeFileSync(`./posts/${filename}`, md); 86 | }); 87 | } 88 | -------------------------------------------------------------------------------- /img/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pioug/blog/d851c33d2f072f653659e96e1bf550b918270ebb/img/.gitkeep -------------------------------------------------------------------------------- /index.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home.njk 3 | tags: 4 | - nav 5 | navtitle: Home 6 | --- 7 | 8 |

Home

9 | {% set postslist = collections.posts %} 10 | {% include "postslist.njk" %} 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blog", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "build": "npx eleventy", 6 | "serve": "npx eleventy --serve", 7 | "watch": "npx eleventy --watch", 8 | "debug": "DEBUG=* npx eleventy" 9 | }, 10 | "devDependencies": { 11 | "@11ty/eleventy": "0.8.3", 12 | "@11ty/eleventy-plugin-rss": "1.0.6", 13 | "@11ty/eleventy-plugin-syntaxhighlight": "2.0.3", 14 | "graphql.js": "0.6.5", 15 | "gulp": "4.0.2", 16 | "luxon": "1.17.2", 17 | "markdown-it": "9.0.1", 18 | "markdown-it-anchor": "5.2.4", 19 | "prettier": "1.18.2", 20 | "slugify": "1.3.4", 21 | "tokenize-english": "1.0.3" 22 | }, 23 | "dependencies": {} 24 | } 25 | -------------------------------------------------------------------------------- /posts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pioug/blog/d851c33d2f072f653659e96e1bf550b918270ebb/posts/.gitkeep -------------------------------------------------------------------------------- /sitemap.xml.njk: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /sitemap.xml 3 | eleventyExcludeFromCollections: true 4 | --- 5 | 6 | 7 | {%- for page in collections.all %} 8 | {% set absoluteUrl %}{{ page.url | url | absoluteUrl(metadata.url) }}{% endset %} 9 | 10 | {{ absoluteUrl }} 11 | {{ page.date | htmlDateString }} 12 | 13 | {%- endfor %} 14 | 15 | -------------------------------------------------------------------------------- /tags.njk: -------------------------------------------------------------------------------- 1 | --- 2 | pagination: 3 | data: collections 4 | size: 1 5 | alias: tag 6 | filter: 7 | - all 8 | - nav 9 | - post 10 | - posts 11 | - tagList 12 | addAllPagesToCollections: true 13 | layout: home.njk 14 | permalink: /tags/{{ tag }}/ 15 | --- 16 |

#{{ tag }}

17 | 18 | {% set postslist = collections[tag] %} 19 | {% include "postslist.njk" %} 20 | 21 |

All tags

22 | -------------------------------------------------------------------------------- /tagslist.njk: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /tags/ 3 | layout: home.njk 4 | --- 5 |

Tags

6 | 7 | 13 | --------------------------------------------------------------------------------