feat: implement dynamic Eleventy tiling blog, modular CSS, and agent customizations
- Add project foundation README.md and root AGENTS.md for pair programming - Configure .agents/ rules and skills for Eleventy tiling and Obsidian publishing - Decompose monolithic styles into modular tokens, reset, layout, and component CSS - Implement Nunjucks layout inheritance (base.njk, post.njk, page.njk) and reusable partials - Configure Eleventy 3 with markdown-it-anchor, syntax highlighting, and dynamic collections - Migrate sample prototypes into dynamic dispatches lander, gateway, and Markdown posts - Add multi-stage Dockerfile and clean-URL nginx.conf for production serving
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
import syntaxHighlight from "@11ty/eleventy-plugin-syntaxhighlight";
|
||||
import markdownIt from "markdown-it";
|
||||
import markdownItAnchor from "markdown-it-anchor";
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
export default function (eleventyConfig) {
|
||||
// 1. Static Asset Passthrough Copy
|
||||
eleventyConfig.addPassthroughCopy({ "src/assets": "assets" });
|
||||
|
||||
// 2. Syntax Highlighting Plugin
|
||||
eleventyConfig.addPlugin(syntaxHighlight);
|
||||
|
||||
// 3. Markdown-It Library Customization
|
||||
const markdownOptions = {
|
||||
html: true,
|
||||
breaks: false,
|
||||
linkify: true
|
||||
};
|
||||
|
||||
const markdownLib = markdownIt(markdownOptions).use(markdownItAnchor, {
|
||||
permalink: markdownItAnchor.permalink.headerLink({
|
||||
safariReaderFix: true
|
||||
}),
|
||||
slugify: (s) =>
|
||||
s
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9 -]/g, "")
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
});
|
||||
|
||||
eleventyConfig.setLibrary("md", markdownLib);
|
||||
|
||||
// 4. Custom Filters
|
||||
// Format readable dates: SEP 12, 2026
|
||||
eleventyConfig.addFilter("postDate", (dateObj) => {
|
||||
if (!dateObj) return "";
|
||||
return DateTime.fromJSDate(new Date(dateObj), { zone: "utc" })
|
||||
.toFormat("LLL dd, yyyy")
|
||||
.toUpperCase();
|
||||
});
|
||||
|
||||
// ISO date format for <time datetime="...">
|
||||
eleventyConfig.addFilter("isoDate", (dateObj) => {
|
||||
if (!dateObj) return "";
|
||||
return DateTime.fromJSDate(new Date(dateObj), { zone: "utc" }).toISODate();
|
||||
});
|
||||
|
||||
// Limit array items
|
||||
eleventyConfig.addFilter("limit", (arr, limit) => {
|
||||
if (!Array.isArray(arr)) return [];
|
||||
return arr.slice(0, limit);
|
||||
});
|
||||
|
||||
// Filter out internal system tags
|
||||
eleventyConfig.addFilter("filterTagList", (tags) => {
|
||||
const internalTags = ["all", "nav", "post", "posts"];
|
||||
return (tags || []).filter((tag) => !internalTags.includes(tag.toLowerCase()));
|
||||
});
|
||||
|
||||
// Reading time estimator (defaults to 200 words/min if frontmatter lacks readingTime)
|
||||
eleventyConfig.addFilter("readingTime", (text, frontmatterTime) => {
|
||||
if (frontmatterTime) return frontmatterTime;
|
||||
if (!text) return "1 MIN READ";
|
||||
const wordCount = text.trim().split(/\s+/).length;
|
||||
const minutes = Math.ceil(wordCount / 200);
|
||||
return `${minutes} MIN READ`;
|
||||
});
|
||||
|
||||
// Extract H2 and H3 headings with IDs for dynamic Table of Contents
|
||||
eleventyConfig.addFilter("extractHeadings", (htmlContent) => {
|
||||
if (!htmlContent) return [];
|
||||
const headingRegex = /<h([23])\s+id="([^"]+)"[^>]*>(.*?)<\/h\1>/gis;
|
||||
const headings = [];
|
||||
let match;
|
||||
while ((match = headingRegex.exec(htmlContent)) !== null) {
|
||||
headings.push({
|
||||
level: parseInt(match[1], 10),
|
||||
id: match[2],
|
||||
title: match[3].replace(/<[^>]+>/g, "").trim()
|
||||
});
|
||||
}
|
||||
return headings;
|
||||
});
|
||||
|
||||
// 5. Custom Collections
|
||||
// Chronological blog posts collection
|
||||
eleventyConfig.addCollection("posts", (collectionApi) => {
|
||||
return collectionApi
|
||||
.getFilteredByGlob("src/posts/*.md")
|
||||
.filter((post) => !post.data.draft)
|
||||
.sort((a, b) => b.date - a.date);
|
||||
});
|
||||
|
||||
// Dynamic Tag List Collection across all posts
|
||||
eleventyConfig.addCollection("tagList", (collectionApi) => {
|
||||
const tagSet = new Set();
|
||||
collectionApi.getFilteredByGlob("src/posts/*.md").forEach((item) => {
|
||||
if ("tags" in item.data) {
|
||||
let tags = item.data.tags;
|
||||
if (typeof tags === "string") tags = [tags];
|
||||
for (const tag of tags) {
|
||||
const cleanTag = tag.replace(/^#/, "");
|
||||
tagSet.add(cleanTag);
|
||||
}
|
||||
}
|
||||
});
|
||||
return [...tagSet].sort();
|
||||
});
|
||||
|
||||
return {
|
||||
dir: {
|
||||
input: "src",
|
||||
includes: "_includes",
|
||||
data: "_data",
|
||||
output: "_site"
|
||||
},
|
||||
templateFormats: ["njk", "md", "html"],
|
||||
htmlTemplateEngine: "njk",
|
||||
markdownTemplateEngine: "njk"
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user