feat: created basic blog layout
This commit is contained in:
parent
42fd071301
commit
0d41db36ef
13 changed files with 935 additions and 3 deletions
25
_includes/base.njk
Normal file
25
_includes/base.njk
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ title }} | Volpe</title>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<link rel="stylesheet" href="/css/prism.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav>
|
||||
<a href="/">Home</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
{{ content | safe }}
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>© {{ page.date.getFullYear() }} Volpe</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
30
_includes/post.njk
Normal file
30
_includes/post.njk
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
layout: base.njk
|
||||
---
|
||||
|
||||
<article class="blog-post">
|
||||
<header class="post-header">
|
||||
<h1>{{ title }}</h1>
|
||||
<time datetime="{{ createdAt | dateFormat }}">{{ createdAt | dateFormat }}</time>
|
||||
{% if updatedAt %}
|
||||
<p class="updated-at">Updated: <time datetime="{{ updatedAt | dateFormat }}">{{ updatedAt | dateFormat }}</time></p>
|
||||
{% endif %}
|
||||
</header>
|
||||
|
||||
<div class="post-content">
|
||||
{{ content | safe }}
|
||||
</div>
|
||||
|
||||
{% set postTags = page.inputPath | extractTagsFromFile %}
|
||||
|
||||
{% if postTags.length > 0 %}
|
||||
<section class="post-tags">
|
||||
<h2>Tags</h2>
|
||||
<ul class="tag-list">
|
||||
{% for tag in postTags %}
|
||||
<li><a href="/tags/{{ tag | lower }}/">#{{ tag }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
{% endif %}
|
||||
</article>
|
||||
149
css/prism.css
Normal file
149
css/prism.css
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
/* PrismJS 1.29.0 - One Dark Theme */
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
color: #abb2bf;
|
||||
background: none;
|
||||
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
line-height: 1.5;
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
pre[class*="language-"] {
|
||||
padding: 1em;
|
||||
margin: 1rem 0;
|
||||
overflow: auto;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
:not(pre) > code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
background: #282c34;
|
||||
}
|
||||
|
||||
:not(pre) > code[class*="language-"] {
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: 0.3em;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: #5c6370;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #abb2bf;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.tag {
|
||||
color: #e06c75;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.boolean,
|
||||
.token.number,
|
||||
.token.constant,
|
||||
.token.symbol,
|
||||
.token.attr-name,
|
||||
.token.deleted {
|
||||
color: #d19a66;
|
||||
}
|
||||
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.attr-value,
|
||||
.token.builtin,
|
||||
.token.inserted {
|
||||
color: #98c379;
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url,
|
||||
.language-css .token.string,
|
||||
.style .token.string {
|
||||
color: #56b6c2;
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.keyword {
|
||||
color: #c678dd;
|
||||
}
|
||||
|
||||
.token.function,
|
||||
.token.class-name {
|
||||
color: #61afef;
|
||||
}
|
||||
|
||||
.token.regex,
|
||||
.token.important,
|
||||
.token.variable {
|
||||
color: #c678dd;
|
||||
}
|
||||
|
||||
.token.important,
|
||||
.token.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
/* Line numbers */
|
||||
pre[class*="language-"].line-numbers {
|
||||
position: relative;
|
||||
padding-left: 3.8em;
|
||||
counter-reset: linenumber;
|
||||
}
|
||||
|
||||
pre[class*="language-"].line-numbers > code {
|
||||
position: relative;
|
||||
white-space: inherit;
|
||||
}
|
||||
|
||||
.line-numbers .line-numbers-rows {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
top: 0;
|
||||
font-size: 100%;
|
||||
left: -3.8em;
|
||||
width: 3em;
|
||||
letter-spacing: -1px;
|
||||
border-right: 1px solid #5c6370;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.line-numbers-rows > span {
|
||||
display: block;
|
||||
counter-increment: linenumber;
|
||||
}
|
||||
|
||||
.line-numbers-rows > span:before {
|
||||
content: counter(linenumber);
|
||||
color: #5c6370;
|
||||
display: block;
|
||||
padding-right: 0.8em;
|
||||
text-align: right;
|
||||
}
|
||||
259
css/style.css
Normal file
259
css/style.css
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
/* Base styles */
|
||||
:root {
|
||||
--primary-color: #2c3e50;
|
||||
--secondary-color: #3498db;
|
||||
--text-color: #333;
|
||||
--background-color: #fff;
|
||||
--border-color: #ddd;
|
||||
--code-background: #f5f5f5;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: var(--text-color);
|
||||
background-color: var(--background-color);
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
/* Header and Navigation */
|
||||
header {
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
header nav a {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
header nav a:hover {
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
/* Main content */
|
||||
main {
|
||||
min-height: calc(100vh - 200px);
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: var(--primary-color);
|
||||
margin: 1.5rem 0 1rem;
|
||||
}
|
||||
|
||||
h1 { font-size: 2rem; }
|
||||
h2 { font-size: 1.5rem; }
|
||||
h3 { font-size: 1.25rem; }
|
||||
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
/* Blog post list */
|
||||
.post-list {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.post-list li {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.post-list li:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.post-card {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.post-card:hover {
|
||||
border-color: var(--secondary-color);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.post-card h2 {
|
||||
margin: 0 0 0.5rem;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.post-card:hover h2 {
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.post-card time {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.post-card p {
|
||||
color: var(--text-color);
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Blog post */
|
||||
.blog-post {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.post-header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.post-header h1 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.post-header time {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.post-content {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.post-content img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.post-content code {
|
||||
background-color: var(--code-background);
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.post-content pre {
|
||||
background-color: var(--code-background);
|
||||
padding: 1rem;
|
||||
border-radius: 5px;
|
||||
overflow-x: auto;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.post-content pre code {
|
||||
padding: 0;
|
||||
background: none;
|
||||
}
|
||||
|
||||
/* Expandable sections */
|
||||
.expandable {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 5px;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.expandable summary {
|
||||
padding: 0.75rem 1rem;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
background-color: var(--code-background);
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
|
||||
.expandable[open] summary {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.expandable > *:not(summary) {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.expandable > p:last-child {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Tags */
|
||||
.post-tags {
|
||||
margin-top: 2rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.post-tags h2 {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.tag-list {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.tag-list li {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.tag-list a {
|
||||
color: var(--secondary-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.tag-list a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Footnotes */
|
||||
.footnotes {
|
||||
margin-top: 2rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 2px solid var(--border-color);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.footnotes h2 {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.footnotes ol {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.footnotes li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.footnote-ref a {
|
||||
color: var(--secondary-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.footnote-backref {
|
||||
margin-left: 0.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
footer {
|
||||
margin-top: 2rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
210
eleventy.config.js
Normal file
210
eleventy.config.js
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
const markdownIt = require("markdown-it");
|
||||
const markdownItContainer = require("markdown-it-container");
|
||||
const markdownItFootnote = require("markdown-it-footnote");
|
||||
const syntaxHighlight = require("@11ty/eleventy-plugin-syntaxhighlight");
|
||||
const fs = require("fs");
|
||||
|
||||
const tagPattern = /(?<=^|\s)#([a-zA-Z][a-zA-Z0-9_]*)(?![a-zA-Z0-9_-])/g;
|
||||
|
||||
// TODO: is there any reasonable way to make this use real markdown parsing because right now this is sketchy
|
||||
const extractTags = (content) => {
|
||||
if (!content) return [];
|
||||
const matches = content.match(tagPattern);
|
||||
if (!matches) return [];
|
||||
const tags = [...new Set(matches.map(m => m.slice(1)))];
|
||||
return tags;
|
||||
}
|
||||
|
||||
const getPostTags = (post) => {
|
||||
const filePath = post.inputPath;
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const tags = extractTags(content);
|
||||
return tags.map(tag => {
|
||||
const normalizedTag = tag.toLowerCase();
|
||||
return normalizedTag
|
||||
});
|
||||
} catch (e) {
|
||||
// Skip if file can't be read
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
const isReleased = (post) => {
|
||||
return post.data.unreleased !== true;
|
||||
}
|
||||
|
||||
const markdownItHashtag = (md) => {
|
||||
const hashtagRegex = /^#([a-zA-Z][a-zA-Z0-9_]*)(?![a-zA-Z0-9_-])/;
|
||||
|
||||
md.inline.ruler.push('hashtag', function(state, silent) {
|
||||
const pos = state.pos;
|
||||
const ch = state.src.charCodeAt(pos);
|
||||
|
||||
if (ch !== '#') return false;
|
||||
|
||||
if (pos > 0) {
|
||||
const prevCh = state.src.charCodeAt(pos - 1);
|
||||
if (prevCh !== ' ' && prevCh !== '\t' && prevCh !== '\n' && prevCh !== '\r') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const match = state.src.slice(pos).match(hashtagRegex);
|
||||
if (!match) return false;
|
||||
|
||||
if (!silent) {
|
||||
const token = state.push('hashtag', 'a', 0);
|
||||
token.content = match[1];
|
||||
token.markup = '#';
|
||||
}
|
||||
|
||||
state.pos += match[0].length;
|
||||
return true;
|
||||
});
|
||||
|
||||
md.renderer.rules.hashtag = function(tokens, idx) {
|
||||
const tagName = tokens[idx].content;
|
||||
const slug = tagName.toLowerCase();
|
||||
return `<a href="/tags/${slug}/" class="inline-tag">#${md.utils.escapeHtml(tagName)}</a>`;
|
||||
};
|
||||
}
|
||||
|
||||
const md = markdownIt({
|
||||
html: true,
|
||||
breaks: true,
|
||||
linkify: true
|
||||
});
|
||||
|
||||
md.use(markdownItContainer, 'details', {
|
||||
validate: function (params) {
|
||||
return params.trim().match(/^(.*)$/);
|
||||
},
|
||||
render: function (tokens, idx) {
|
||||
const m = tokens[idx].info.trim().match(/^(.*)$/);
|
||||
if (tokens[idx].nesting === 1) {
|
||||
const title = md.utils.escapeHtml(m[1]);
|
||||
return `<details class="expandable">\n<summary>${title}</summary>\n`;
|
||||
} else {
|
||||
return '</details>\n';
|
||||
}
|
||||
}
|
||||
});
|
||||
md.use(markdownItFootnote);
|
||||
md.use(markdownItHashtag);
|
||||
|
||||
module.exports = (eleventyConfig) => {
|
||||
eleventyConfig.addPlugin(syntaxHighlight);
|
||||
|
||||
eleventyConfig.addFilter("extractTags", extractTags);
|
||||
eleventyConfig.addFilter("extractTagsFromFile", (filePath) => {
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
return extractTags(content);
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
eleventyConfig.setLibrary("md", md);
|
||||
|
||||
eleventyConfig.addFilter("dateFormat", (date) => {
|
||||
const d = new Date(date);
|
||||
return d.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
timeZone: 'UTC'
|
||||
});
|
||||
});
|
||||
|
||||
eleventyConfig.addFilter("isoDate", (date) => {
|
||||
const d = new Date(date);
|
||||
return d.toISOString().split('T')[0];
|
||||
});
|
||||
|
||||
eleventyConfig.addCollection("posts", (collectionApi) => {
|
||||
return collectionApi.getFilteredByGlob("posts/**/*.md")
|
||||
.filter(isReleased)
|
||||
.sort((a, b) => {
|
||||
const aDate = a.data.createdAt || a.date;
|
||||
const bDate = b.data.createdAt || b.date;
|
||||
return aDate - bDate;
|
||||
});
|
||||
});
|
||||
|
||||
eleventyConfig.addCollection("allPosts", (collectionApi) => {
|
||||
return collectionApi.getFilteredByGlob("posts/**/*.md").sort((a, b) => {
|
||||
const aDate = a.data.createdAt || a.date;
|
||||
const bDate = b.data.createdAt || b.date;
|
||||
return aDate - bDate;
|
||||
});
|
||||
});
|
||||
|
||||
eleventyConfig.addCollection("postsBySlug", (collectionApi) => {
|
||||
const posts = collectionApi.getFilteredByGlob("posts/**/*.md").filter(isReleased);
|
||||
const slugify = eleventyConfig.getFilter("slugify");
|
||||
const grouped = {};
|
||||
|
||||
posts.forEach(post => {
|
||||
const slug = slugify(post.data.title);
|
||||
if (!grouped[slug]) {
|
||||
grouped[slug] = [];
|
||||
}
|
||||
grouped[slug].push(post);
|
||||
});
|
||||
|
||||
Object.values(grouped).forEach(postList => {
|
||||
postList.sort((a, b) => {
|
||||
const aDate = a.data.createdAt || a.date;
|
||||
const bDate = b.data.createdAt || b.date;
|
||||
return aDate - bDate;
|
||||
});
|
||||
});
|
||||
|
||||
return grouped;
|
||||
});
|
||||
|
||||
eleventyConfig.addCollection("contentTags", (collectionApi) => {
|
||||
const posts = collectionApi.getFilteredByGlob("posts/**/*.md").filter(isReleased);
|
||||
|
||||
return [...new Set(posts.flatMap(getPostTags))].sort();
|
||||
});
|
||||
|
||||
eleventyConfig.addCollection("postsByTag", (collectionApi) => {
|
||||
const posts = collectionApi.getFilteredByGlob("posts/**/*.md").filter(isReleased);
|
||||
const tagMap = {};
|
||||
|
||||
posts.forEach(post => {
|
||||
const tags = getPostTags(post)
|
||||
tags.forEach((tag) => {
|
||||
tagMap[tag] = {
|
||||
name: tag,
|
||||
posts: [post, ...(tagMap[tag]?.posts ?? [])],
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
Object.values(tagMap).forEach(tagData => {
|
||||
tagData.posts.sort((a, b) => {
|
||||
const aDate = a.data.createdAt || a.date;
|
||||
const bDate = b.data.createdAt || b.date;
|
||||
return aDate - bDate;
|
||||
});
|
||||
});
|
||||
|
||||
return tagMap;
|
||||
});
|
||||
|
||||
eleventyConfig.addPassthroughCopy("css");
|
||||
|
||||
return {
|
||||
dir: {
|
||||
input: ".",
|
||||
includes: "_includes",
|
||||
output: "_site"
|
||||
},
|
||||
markdownTemplateEngine: "njk",
|
||||
htmlTemplateEngine: "njk"
|
||||
};
|
||||
};
|
||||
|
|
@ -1 +0,0 @@
|
|||
<!doctype html><title>Page title</title><p>Hi</p>
|
||||
24
index.njk
Normal file
24
index.njk
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
layout: base.njk
|
||||
title: Blog
|
||||
---
|
||||
|
||||
<h1>Blog Posts</h1>
|
||||
|
||||
<ul class="post-list">
|
||||
{% for post in collections.posts %}
|
||||
<li>
|
||||
<a href="{{ post.url }}" class="post-card">
|
||||
<h2>{{ post.data.title }}</h2>
|
||||
<time datetime="{{ post.data.createdAt | dateFormat }}">{{ post.data.createdAt | dateFormat }}</time>
|
||||
{% if post.data.description %}
|
||||
<p>{{ post.data.description }}</p>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% if collections.posts.length == 0 %}
|
||||
<p>No blog posts yet.</p>
|
||||
{% endif %}
|
||||
|
|
@ -5,13 +5,17 @@
|
|||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "pnpm exec eleventy",
|
||||
"develop": "pnpm exec eleventy --serve"
|
||||
"dev": "pnpm exec eleventy --serve"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "GPL-3.0-only",
|
||||
"packageManager": "pnpm@10.28.0",
|
||||
"dependencies": {
|
||||
"@11ty/eleventy": "^3.1.2"
|
||||
"@11ty/eleventy": "^3.1.2",
|
||||
"@11ty/eleventy-plugin-syntaxhighlight": "^5.0.2",
|
||||
"markdown-it": "^14.1.0",
|
||||
"markdown-it-container": "^4.0.0",
|
||||
"markdown-it-footnote": "^4.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
35
pnpm-lock.yaml
generated
35
pnpm-lock.yaml
generated
|
|
@ -11,6 +11,18 @@ importers:
|
|||
'@11ty/eleventy':
|
||||
specifier: ^3.1.2
|
||||
version: 3.1.2
|
||||
'@11ty/eleventy-plugin-syntaxhighlight':
|
||||
specifier: ^5.0.2
|
||||
version: 5.0.2
|
||||
markdown-it:
|
||||
specifier: ^14.1.0
|
||||
version: 14.1.0
|
||||
markdown-it-container:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
markdown-it-footnote:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
|
||||
packages:
|
||||
|
||||
|
|
@ -29,6 +41,9 @@ packages:
|
|||
resolution: {integrity: sha512-QK1tRFBhQdZASnYU8GMzpTdsMMFLVAkuU0gVVILqNyp09xJJZb81kAS3AFrNrwBCsgLxTdWHJ8N64+OTTsoKkA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@11ty/eleventy-plugin-syntaxhighlight@5.0.2':
|
||||
resolution: {integrity: sha512-T6xVVRDJuHlrFMHbUiZkHjj5o1IlLzZW+1IL9eUsyXFU7rY2ztcYhZew/64vmceFFpQwzuSfxQOXxTJYmKkQ+A==}
|
||||
|
||||
'@11ty/eleventy-utils@2.0.7':
|
||||
resolution: {integrity: sha512-6QE+duqSQ0GY9rENXYb4iPR4AYGdrFpqnmi59tFp9VrleOl0QSh8VlBr2yd6dlhkdtj7904poZW5PvGr9cMiJQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -351,6 +366,12 @@ packages:
|
|||
resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
markdown-it-container@4.0.0:
|
||||
resolution: {integrity: sha512-HaNccxUH0l7BNGYbFbjmGpf5aLHAMTinqRZQAEQbMr2cdD3z91Q6kIo1oUn1CQndkT03jat6ckrdRYuwwqLlQw==}
|
||||
|
||||
markdown-it-footnote@4.0.0:
|
||||
resolution: {integrity: sha512-WYJ7urf+khJYl3DqofQpYfEYkZKbmXmwxQV8c8mO/hGIhgZ1wOe7R4HLFNwqx7TjILbnC98fuyeSsin19JdFcQ==}
|
||||
|
||||
markdown-it@14.1.0:
|
||||
resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
|
||||
hasBin: true
|
||||
|
|
@ -454,6 +475,10 @@ packages:
|
|||
resolution: {integrity: sha512-7Hc+IvlQ7hlaIfQFZnxlRl0jnpWq2qwibORBhQYIb0QbNtuicc5ZxvKkVT71HJ4Py1wSZ/3VR1r8LfkCtoCzhw==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
prismjs@1.30.0:
|
||||
resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
prr@1.0.1:
|
||||
resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==}
|
||||
|
||||
|
|
@ -586,6 +611,10 @@ snapshots:
|
|||
- posthtml
|
||||
- supports-color
|
||||
|
||||
'@11ty/eleventy-plugin-syntaxhighlight@5.0.2':
|
||||
dependencies:
|
||||
prismjs: 1.30.0
|
||||
|
||||
'@11ty/eleventy-utils@2.0.7': {}
|
||||
|
||||
'@11ty/eleventy@3.1.2':
|
||||
|
|
@ -903,6 +932,10 @@ snapshots:
|
|||
|
||||
luxon@3.7.2: {}
|
||||
|
||||
markdown-it-container@4.0.0: {}
|
||||
|
||||
markdown-it-footnote@4.0.0: {}
|
||||
|
||||
markdown-it@14.1.0:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
|
|
@ -994,6 +1027,8 @@ snapshots:
|
|||
posthtml-parser: 0.11.0
|
||||
posthtml-render: 3.0.0
|
||||
|
||||
prismjs@1.30.0: {}
|
||||
|
||||
prr@1.0.1: {}
|
||||
|
||||
punycode.js@2.3.1: {}
|
||||
|
|
|
|||
36
post-redirects.njk
Normal file
36
post-redirects.njk
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
pagination:
|
||||
data: collections.postsBySlug
|
||||
size: 1
|
||||
alias: slugData
|
||||
resolve: keys
|
||||
permalink: /post/{{ slugData }}/
|
||||
layout: base.njk
|
||||
---
|
||||
|
||||
{% set posts = collections.postsBySlug[slugData] %}
|
||||
|
||||
{% if posts.length == 1 %}
|
||||
{# Single post - redirect to the dated version #}
|
||||
<meta http-equiv="refresh" content="0; url=/post/{{ slugData }}/{{ posts[0].data.createdAt | isoDate }}/">
|
||||
<script>window.location.href = "/post/{{ slugData }}/{{ posts[0].data.createdAt | isoDate }}/";</script>
|
||||
<p>Redirecting to <a href="/post/{{ slugData }}/{{ posts[0].data.createdAt | isoDate }}/">{{ posts[0].data.title }}</a>...</p>
|
||||
{% else %}
|
||||
{# Multiple posts with same title - show disambiguation page #}
|
||||
<h1>{{ posts[0].data.title }}</h1>
|
||||
<p>There are multiple posts with this title. Please select the one you're looking for:</p>
|
||||
|
||||
<ul class="post-list">
|
||||
{% for post in posts %}
|
||||
<li>
|
||||
<a href="/post/{{ slugData }}/{{ post.data.createdAt | isoDate }}/" class="post-card">
|
||||
<h2>{{ post.data.title }}</h2>
|
||||
<time datetime="{{ post.data.createdAt | dateFormat }}">{{ post.data.createdAt | dateFormat }}</time>
|
||||
{% if post.data.description %}
|
||||
<p>{{ post.data.description }}</p>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
122
posts/posts.11tydata.js
Normal file
122
posts/posts.11tydata.js
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const { execSync } = require("child_process");
|
||||
|
||||
const getGitCreatedTime = (filePath) => {
|
||||
try {
|
||||
const result = execSync(
|
||||
`git log --diff-filter=A --follow --format=%aI -- "${filePath}" | tail -1`,
|
||||
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
||||
).trim();
|
||||
if (result) {
|
||||
return new Date(result);
|
||||
}
|
||||
} catch (e) {
|
||||
// Git command failed, return null
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const getGitModifiedTime = (filePath) => {
|
||||
try {
|
||||
const result = execSync(
|
||||
`git log -1 --format=%aI -- "${filePath}"`,
|
||||
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
||||
).trim();
|
||||
if (result) {
|
||||
return new Date(result);
|
||||
}
|
||||
} catch (e) {
|
||||
// Git command failed, return null
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const getTitleFromFilename = (filePath) => {
|
||||
const basename = path.basename(filePath, '.md');
|
||||
return basename
|
||||
.split('-')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
const getFileCreatedTime = (filePath) => {
|
||||
try {
|
||||
const stats = fs.statSync(filePath);
|
||||
return stats.birthtime || stats.mtime;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const getFileModifiedTime = (filePath) => {
|
||||
try {
|
||||
const stats = fs.statSync(filePath);
|
||||
return stats.mtime;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const parseDate = (value) => {
|
||||
if (!value) return null;
|
||||
if (value instanceof Date) return value;
|
||||
const parsed = new Date(value);
|
||||
return isNaN(parsed.getTime()) ? null : parsed;
|
||||
}
|
||||
|
||||
const getPostCreatedAt = (data) => {
|
||||
const frontmatterDate = parseDate(data.createdAt);
|
||||
if (frontmatterDate) {
|
||||
return frontmatterDate;
|
||||
}
|
||||
const gitDate = getGitCreatedTime(data.page.inputPath);
|
||||
if (gitDate) {
|
||||
return gitDate;
|
||||
}
|
||||
const fileDate = getFileCreatedTime(data.page.inputPath);
|
||||
if (fileDate) {
|
||||
return fileDate;
|
||||
}
|
||||
|
||||
return data.page.date;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
layout: "post.njk",
|
||||
tags: ["posts"],
|
||||
unreleased: false,
|
||||
eleventyComputed: {
|
||||
title: (data) => {
|
||||
if (data.title && data.title !== data.page.fileSlug) {
|
||||
return data.title;
|
||||
}
|
||||
return getTitleFromFilename(data.page.inputPath);
|
||||
},
|
||||
createdAt: getPostCreatedAt,
|
||||
updatedAt: (data) => {
|
||||
const modifiedDate = parseDate(data.updatedAt) ??
|
||||
getGitModifiedTime(data.page.inputPath) ??
|
||||
getFileModifiedTime(data.page.inputPath);
|
||||
const createdDate = getPostCreatedAt(data);
|
||||
|
||||
if (modifiedDate) {
|
||||
// Only return updatedAt if it's AFTER created date (by more than a day)
|
||||
const dayInMs = 24 * 60 * 60 * 1000;
|
||||
if (modifiedDate > createdDate && (modifiedDate - createdDate) > dayInMs) {
|
||||
return modifiedDate;
|
||||
}
|
||||
}
|
||||
return createdDate;
|
||||
},
|
||||
permalink: (data) => {
|
||||
const title = data.title || getTitleFromFilename(data.page.inputPath);
|
||||
const slug = title.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]/g, '');
|
||||
|
||||
const createdDate = getPostCreatedAt(data);
|
||||
|
||||
const isoDate = createdDate.toISOString().split('T')[0];
|
||||
return `/post/${slug}/${isoDate}/`;
|
||||
}
|
||||
}
|
||||
};
|
||||
12
tags-index.njk
Normal file
12
tags-index.njk
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
permalink: /tags/
|
||||
layout: base.njk
|
||||
---
|
||||
|
||||
<h1>All Tags</h1>
|
||||
|
||||
<ul class="tag-list">
|
||||
{% for tag in collections.contentTags %}
|
||||
<li><a href="/tags/{{ tag }}/">#{{ tag }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
27
tags.njk
Normal file
27
tags.njk
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
pagination:
|
||||
data: collections.postsByTag
|
||||
size: 1
|
||||
alias: tag
|
||||
addAllPagesToCollections: true
|
||||
permalink: /tags/{{ tag }}/
|
||||
layout: base.njk
|
||||
---
|
||||
|
||||
<h1>Posts tagged "#{{ collections.postsByTag[tag].name }}"</h1>
|
||||
|
||||
<ul class="post-list">
|
||||
{% for post in collections.postsByTag[tag].posts %}
|
||||
<li>
|
||||
<a href="{{ post.url }}" class="post-card">
|
||||
<h2>{{ post.data.title }}</h2>
|
||||
<time datetime="{{ post.date | dateFormat }}">{{ post.date | dateFormat }}</time>
|
||||
{% if post.data.description %}
|
||||
<p>{{ post.data.description }}</p>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<p><a href="/tags/"><3E> All tags</a></p>
|
||||
Loading…
Add table
Add a link
Reference in a new issue