eleventy

Eleventy Configuration

Main configuration in uu_framework/eleventy/.eleventy.js (301 lines).

Markdown Processing

Plugins (lines 6-9)

const markdownIt = require("markdown-it");
const markdownItContainer = require("markdown-it-container");
const markdownItAttrs = require("markdown-it-attrs");
const markdownItAnchor = require("markdown-it-anchor");

Options (lines 17-22)

const mdOptions = {
  html: true,        // Allow raw HTML
  breaks: false,     // Don't convert \n to <br>
  linkify: true,     // Auto-link URLs
  typographer: true  // Smart quotes, dashes
};

Component Container (lines 32-56)

const componentTypes = ['homework', 'exercise', 'prompt', 'example', 'exam', 'project'];

componentTypes.forEach(type => {
  md.use(markdownItContainer, type, {
    validate: (params) => params.trim().match(new RegExp(`^${type}\\s*(.*)$`)),
    render: (tokens, idx) => {
      if (tokens[idx].nesting === 1) {
        // Opening: <div class="component component--TYPE" data-*="...">
        return `<div class="component component--${type}" ${attrsHtml}>\n`;
      } else {
        return '</div>\n';
      }
    }
  });
});

Filters

formatDate (lines 78-86)

eleventyConfig.addFilter("formatDate", function(date) {
  return new Date(date).toLocaleDateString('es-MX', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  });
});
// "2026-02-01" → "1 de febrero de 2026"

titleFromFilename (lines 89-99)

eleventyConfig.addFilter("titleFromFilename", function(filename) {
  const name = filename.split('/').pop().replace(/\.\w+$/, '');
  const withoutPrefix = name.replace(/^\d+[_-]?/, '');
  return withoutPrefix
    .replace(/[_-]/g, ' ')
    .replace(/\b\w/g, l => l.toUpperCase());
});
// "01_install_python.md" → "Install Python"

getOrder (lines 102-106)

eleventyConfig.addFilter("getOrder", function(filename) {
  const match = filename.match(/^(\d+)/);
  return match ? parseInt(match[1], 10) : 999;
});
// "02_intro" → 2

getNavNumber (lines 110-126)

eleventyConfig.addFilter("getNavNumber", function(name, prefix, index) {
  // Letter prefix (appendix): a_stack → "A"
  const letterMatch = name.match(/^([a-z])_/i);
  if (letterMatch) {
    return letterMatch[1].toUpperCase();
  }
  // Numeric prefix: 02_llms → "A.2" (with parent prefix)
  const numMatch = name.match(/^(\d+)[_-]/);
  if (numMatch) {
    return prefix + parseInt(numMatch[1], 10);
  }
  // Fallback
  return prefix + (index + 1);
});

cleanNavTitle (lines 129-139)

eleventyConfig.addFilter("cleanNavTitle", function(title) {
  return title
    .replace(/^Módulo\s*\d+\s*[:\-]\s*/i, '')  // Remove "Módulo X:"
    .replace(/^Module\s*\d+\s*[:\-]\s*/i, '')
    .replace(/^Capítulo\s*\d+\s*[:\-]\s*/i, '')
    .trim();
});
// "Módulo 2: LLMs" → "LLMs"

Collections

content (lines 146-162)

eleventyConfig.addCollection("content", function(collectionApi) {
  return collectionApi.getFilteredByGlob("clase/**/*.md")
    .filter(item => {
      if (item.inputPath.includes('b_libros')) return false;
      if (item.inputPath.includes('README_FLOW')) return false;
      if (item.inputPath.includes('task-pages')) return false;
      if (item.inputPath.includes('??_')) return false;
      return true;
    })
    .sort((a, b) => {
      const orderA = a.data.order || getOrderFromPath(a.inputPath);
      const orderB = b.data.order || getOrderFromPath(b.inputPath);
      return orderA - orderB;
    });
});

Transforms

Converts .md links to / URLs in output HTML:

eleventyConfig.addTransform("fixMdLinks", function(content, outputPath) {
  if (outputPath && outputPath.endsWith(".html")) {
    return content.replace(
      /href="([^"]*?)\.md"/g,
      (match, path) => {
        if (path.startsWith('http')) return match;
        let newPath = path;
        if (newPath.startsWith('./')) {
          newPath = '../' + newPath.slice(2);
        }
        return `href="${newPath}/"`;
      }
    );
  }
  return content;
});

Passthrough Copy

// CSS
eleventyConfig.addPassthroughCopy({ "src/css": "css" });

// Fonts
eleventyConfig.addPassthroughCopy({ "src/fonts": "fonts" });

// Images from content
eleventyConfig.addPassthroughCopy("clase/**/*.{png,jpg,jpeg,gif,svg,webp}");

Shortcodes

icon (lines 169-182)

eleventyConfig.addShortcode("icon", function(name) {
  const icons = {
    homework: '[T]',
    exercise: '[E]',
    prompt: '[>]',
    example: '[*]',
    exam: '[!]',
    project: '[P]'
  };
  return icons[name] || `[${name}]`;
});

Usage: {% icon "homework" %}[T]


Global Data

eleventyConfig.addGlobalData("layout", "layouts/base.njk");

All markdown files use base.njk unless overridden in frontmatter.


Helper Functions

parseAttributes (lines 254-265)

function parseAttributes(str) {
  const attrs = {};
  const regex = /(\w+)=["']([^"']+)["']/g;
  let match;
  while ((match = regex.exec(str)) !== null) {
    attrs[match[1]] = match[2];
  }
  return attrs;
}
// '{id="A.1" title="Test"}' → {id: "A.1", title: "Test"}

getOrderFromPath (lines 272-300)

function getOrderFromPath(path) {
  // Calculates sort order from file path
  // Uses weighted numeric prefixes
  // Letter prefixes (a_, b_) sort after numbers
}