Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: generate & auto insert lqip data into markdown file #2265

Open
1 task done
lotusk08 opened this issue Feb 22, 2025 · 1 comment
Open
1 task done

feat: generate & auto insert lqip data into markdown file #2265

lotusk08 opened this issue Feb 22, 2025 · 1 comment
Labels
enhancement New feature or request

Comments

@lotusk08
Copy link

Checklist

Is your feature request related to a problem? Please describe

LQIP

Describe the solution you'd like

I made a script to generate lqip from images and auto-insert lqip data in blog posts/tabs pages. That handles both frontmatter and content. Like this:

For yaml example, it will process:

---
image:
  path: /assets/img/post/sonadezi.webp
  alt: Sân golf Sonadezi
---

And update it to:

---
image:
  path: /assets/img/post/sonadezi.webp
  lqip: data:image/webp;base64,UklGR...
  alt: Sân golf Sonadezi
---

For the content, handle cases like:

![content](/assets/img/post/design-work.webp)

And convert to:

![content](/assets/img/post/design-work.webp){: lqip="data:image/webp;base64,..." }

Or if there are existing attributes:

![content](/assets/img/post/design-work.webp){: w='680' }

Will become:

![content](/assets/img/post/design-work.webp){: lqip="data:image/webp;base64,..." w='680' }

How:

  1. Install 2 dependencies:
    "lqip-modern": "^2.2.1",
    "js-yaml": "^4.1.0"

  2. Create a JS script in folder tools/
    I named it update-lqip.js:

const fs = require("fs");
const path = require("path");
const yaml = require("js-yaml");

// Function to process front matter YAML and update LQIP
async function processFrontMatter(content, lqipMap) {
  const frontMatterRegex = /^---\n([\s\S]*?)\n---/;
  const match = content.match(frontMatterRegex);

  if (match) {
    try {
      const frontMatter = yaml.load(match[1]);

      if (frontMatter.image && frontMatter.image.path) {
        const imagePath = frontMatter.image.path;
        const lqipData = lqipMap[imagePath];

        if (lqipData) {
          frontMatter.image.lqip = lqipData;
          const newFrontMatter = yaml.dump(frontMatter, {
            lineWidth: -1,
            quotingType: '"',
            forceQuotes: false,
            noRefs: true,
            skipInvalid: true,
            styles: {
              "!!str": "LITERAL"
            }
          });
          return content.replace(frontMatterRegex, `---\n${newFrontMatter}---`);
        }
      }
    } catch (err) {
      console.error("Error processing front matter:", err);
    }
  }
  return content;
}

// Function to process Markdown content and update image LQIP
async function processMarkdown(mdPath, lqipMap) {
  let content = await fs.promises.readFile(mdPath, "utf8");

  // Process front matter first
  content = await processFrontMatter(content, lqipMap);

  // Updated regex to handle image with title
  const imageRegex =
    /!\[.*?\]\((\/assets\/img\/.*?\.webp)(?:\s+"[^"]*")?\)({:[^}]*})?/g;

  content = content.replace(imageRegex, (match, imagePath, existingAttrs) => {
    const lqipData = lqipMap[imagePath];
    if (lqipData) {
      // Preserve title if it exists
      const titleMatch = match.match(/\(([^)]+)\s+"([^"]+)"\)/);
      const title = titleMatch ? ` "${titleMatch[2]}"` : "";

      if (existingAttrs) {
        // Extract existing attributes, removing any duplicate lqip
        const attrs = existingAttrs
          .slice(2, -1)
          .replace(/\s*lqip="[^"]*"\s*/g, " ")
          .trim();
        return `![${match.split("![")[1].split("]")[0]}](${imagePath}${title}){: lqip="${lqipData}" ${attrs}}`;
      } else {
        return `![${match.split("![")[1].split("]")[0]}](${imagePath}${title}){: lqip="${lqipData}" }`;
      }
    }
    return match;
  });

  await fs.promises.writeFile(mdPath, content, "utf8");
}

// Function to scan images and generate LQIP data
async function generateLQIP(baseDir) {
  console.log("Starting LQIP generation...");
  const lqip = (await import("lqip-modern")).default;
  const lqipMap = {};

  async function scanDir(dirPath) {
    const dir = await fs.promises.opendir(dirPath);
    for await (const file of dir) {
      const fullPath = path.join(dirPath, file.name);
      if (file.isDirectory()) {
        await scanDir(fullPath);
      } else if (file.name.endsWith(".webp")) {
        try {
          const result = await lqip(fullPath);
          // Create Jekyll-style path (/assets/img/...)
          const jekyllPath =
            "/" + path.relative(baseDir, fullPath).split(path.sep).join("/");

          lqipMap[jekyllPath] = result.metadata.dataURIBase64;
          console.log(`Generated LQIP for: ${jekyllPath}`);
        } catch (err) {
          console.error(`Error processing file ${fullPath}:`, err);
        }
      }
    }
  }

  await scanDir(path.join(baseDir, "assets", "img"));
  return lqipMap;
}

// Function to scan markdown directories and update LQIP
async function updateMarkdownLQIP(markdownDir, lqipMap) {
  const dir = await fs.promises.opendir(markdownDir);
  for await (const file of dir) {
    if (file.name.endsWith(".md")) {
      const mdPath = path.join(markdownDir, file.name);
      try {
        await processMarkdown(mdPath, lqipMap);
        console.log(`Updated LQIP in: ${file.name}`);
      } catch (err) {
        console.error(`Error processing markdown ${mdPath}:`, err);
      }
    }
  }
}

// Main execution
async function main() {
  try {
    // Get the project root directory
    const projectRoot = process.cwd();

    // Generate LQIP data for all images
    const lqipMap = await generateLQIP(projectRoot);

    // Update markdown files in both _posts and _tabs directories
    await updateMarkdownLQIP(path.join(projectRoot, "_posts"), lqipMap);
    await updateMarkdownLQIP(path.join(projectRoot, "_tabs"), lqipMap);

    console.log("LQIP processing completed successfully!");
  } catch (err) {
    console.error("Error:", err);
  }
}

main();
  1. Run it:
node tools/update-lqip.js

My repo: https://github.com/lotusk08/lotusk08.github.io
My site: https://stevehoang.com

Describe alternatives you've considered

No response

Additional context

Image
@lotusk08 lotusk08 added the enhancement New feature or request label Feb 22, 2025
@SoumyaK4
Copy link

Looks great

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants