diff --git a/.eleventy.js b/.eleventy.js
index 53d820b8..e3af982a 100644
--- a/.eleventy.js
+++ b/.eleventy.js
@@ -1,10 +1,11 @@
const { exec } = require("child_process");
const glob = require("fast-glob");
const { DateTime } = require("luxon");
+const slugify = require('slugify');
const fs = require("fs");
const puppeteer = require('puppeteer');
-const slugify = require('slugify');
const path = require('path');
+require('dotenv').config(); // Load environment variables
const pluginAddIdToHeadings = require("@orchidjs/eleventy-plugin-ids");
const pluginRss = require("@11ty/eleventy-plugin-rss");
@@ -53,13 +54,6 @@ module.exports = function (eleventyConfig) {
eleventyConfig.ignores.add("src/nl/vereniging/bestuur/notulen");
}
- // Custom date filter
- eleventyConfig.addFilter("localizedDate", function (dateObj, locale = "en") {
- return DateTime.fromJSDate(dateObj)
- .setLocale(locale)
- .toFormat("d LLLL yyyy");
- });
-
/* Add id to heading elements */
eleventyConfig.addPlugin(pluginAddIdToHeadings);
@@ -158,6 +152,47 @@ module.exports = function (eleventyConfig) {
}
);
+ // Import listmonkUtilities
+ const listmonkUtils = require('./utils/listmonk.js');
+
+ eleventyConfig.on('afterBuild', async () => {
+ const postsDir = path.join(__dirname, 'src/nl/activiteiten'); // Root folder
+ const outputFile = path.join(__dirname, 'dist/latest-post.json'); // Output file
+
+ // Ensure output directory exists
+ if (!fs.existsSync(path.dirname(outputFile))) {
+ fs.mkdirSync(path.dirname(outputFile), { recursive: true });
+ }
+
+ // Find all markdown files recursively
+ const files = await glob([`${postsDir}/**/*.md`]); // Traverse all subdirectories for .md files
+
+ // Parse files and extract front matter date
+ const filteredFiles = files
+ .filter(file => path.basename(file) !== 'index.md') // Exclude index.md
+ .map(file => {
+ const content = fs.readFileSync(file, 'utf-8'); // Read file content
+ const metadata = listmonkUtils.extractFrontMatter(content); // Extract front matter
+ const date = metadata.date ? new Date(metadata.date) : new Date(0); // Parse date or fallback
+ return { file, metadata, date }; // Store file data
+ })
+ .sort((a, b) => b.date - a.date); // Sort by date (most recent first)
+
+ if (filteredFiles.length > 0) {
+ const latestPost = filteredFiles[0]; // Get the most recent post
+ console.log('Latest Post Metadata:', latestPost.metadata); // Debugging log
+
+ // Write latest post metadata to JSON
+ fs.writeFileSync(outputFile, JSON.stringify(latestPost.metadata, null, 2));
+ console.log('Latest post JSON generated:', latestPost.metadata);
+
+ // Check and send campaign (you already have this function)
+ listmonkUtils.checkAndSendCampaign(latestPost.metadata);
+ } else {
+ console.log('No valid posts found.');
+ }
+ });
+
eleventyConfig.setLiquidOptions({
dynamicPartials: false,
strictFilters: false,
@@ -185,111 +220,65 @@ module.exports = function (eleventyConfig) {
})
);
-
- eleventyConfig.addFilter("readablePostDate", (dateObj) => {
- return DateTime.fromJSDate(dateObj, {
- zone: "Europe/Amsterdam",
- }).setLocale('en').toLocaleString(DateTime.DATE_FULL);
- });
-
- eleventyConfig.addFilter("postDate", (dateObj) => {
- return DateTime.fromJSDate(dateObj, {
- zone: "Europe/Amsterdam",
- }).setLocale('en').toISODate();
- });
-
- eleventyConfig.addFilter('splitlines', function (input) {
- const parts = input.split(' ');
- const lines = parts.reduce(function (prev, current) {
-
- if (!prev.length) {
- return [current];
- }
-
- let lastOne = prev[prev.length - 1];
-
- if (lastOne.length + current.length > 23) {
- return [...prev, current];
- }
-
- prev[prev.length - 1] = lastOne + ' ' + current;
-
- return prev;
- }, []);
-
- return lines;
- });
-
- eleventyConfig.on('afterBuild', async () => {
- async function convertSvgToJpeg(inputDir, outputDir) {
- const browser = await puppeteer.launch();
- const page = await browser.newPage();
-
- // Read all files in the input directory
- const files = fs.readdirSync(inputDir);
-
- for (const filename of files) {
- if (filename.endsWith(".svg")) {
- const inputPath = path.join(inputDir, filename);
- const outputPath = path.join(outputDir, filename.replace('.svg', '.jpg'));
-
- // Read the SVG content
- const svgContent = fs.readFileSync(inputPath, 'utf8');
-
- // Extract width and height from SVG (Optional: If SVG has explicit size)
- const matchWidth = svgContent.match(/width="([0-9]+)"/);
- const matchHeight = svgContent.match(/height="([0-9]+)"/);
-
- const width = matchWidth ? parseInt(matchWidth[1], 10) : 1200; // Default to 1200px
- const height = matchHeight ? parseInt(matchHeight[1], 10) : 675; // Default to 630px
-
- // Set the viewport size to match SVG size
- await page.setViewport({ width, height });
-
- // Set SVG content inside an HTML wrapper
- await page.setContent(`
-
-
-
- ${svgContent}
-
-
-
- `);
-
- // Take a screenshot and save as JPEG
- await page.screenshot({
- path: outputPath,
- type: 'jpeg',
- quality: 100,
- clip: { x: 0, y: 0, width, height } // Ensure clipping matches viewport
- });
-
- console.log(`Converted: ${filename} -> ${outputPath}`);
+ if (!quick) {
+ eleventyConfig.on('afterBuild', async () => {
+ async function convertSvgToJpeg(inputDir, outputDir) {
+ const browser = await puppeteer.launch();
+ const page = await browser.newPage();
+
+ // Read all files in the input directory
+ const files = fs.readdirSync(inputDir);
+
+ for (const filename of files) {
+ if (filename.endsWith(".svg")) {
+ const inputPath = path.join(inputDir, filename);
+ const outputPath = path.join(outputDir, filename.replace('.svg', '.jpg'));
+
+ // Read the SVG content
+ const svgContent = fs.readFileSync(inputPath, 'utf8');
+
+ // Extract width and height from SVG (Optional: If SVG has explicit size)
+ const matchWidth = svgContent.match(/width="([0-9]+)"/);
+ const matchHeight = svgContent.match(/height="([0-9]+)"/);
+
+ const width = matchWidth ? parseInt(matchWidth[1], 10) : 1200; // Default to 1200px
+ const height = matchHeight ? parseInt(matchHeight[1], 10) : 675; // Default to 630px
+
+ // Set the viewport size to match SVG size
+ await page.setViewport({ width, height });
+
+ // Set SVG content inside an HTML wrapper
+ await page.setContent(`
+
+
+
+ ${svgContent}
+
+
+
+ `);
+
+ // Take a screenshot and save as JPEG
+ await page.screenshot({
+ path: outputPath,
+ type: 'jpeg',
+ quality: 100,
+ clip: { x: 0, y: 0, width, height } // Ensure clipping matches viewport
+ });
+
+ console.log(`Converted: ${filename} -> ${outputPath}`);
+ }
}
- }
-
- await browser.close();
- }
- // Execute conversion
- const inputDir = 'dist/assets/images/social-preview-images/';
- const outputDir = 'dist/assets/images/social-preview-images/';
- await convertSvgToJpeg(inputDir, outputDir);
- });
-
- // Allows you to debug a json object in eleventy templates data | stringify
- eleventyConfig.addFilter("stringify", (data) => {
- return JSON.stringify(data, null, "\t");
- });
+ await browser.close();
+ }
- eleventyConfig.addFilter("customSlug", function (value) {
- if (!value) return "fallback-title"; // Fallback for empty titles
- return slugify(value, {
- lower: true, // Convert to lowercase
- remove: /[^\w\s-]/g // Remove all non-word characters except spaces and dashes
- }).replace(/\s+/g, '-'); // Replace spaces with dashes (extra safety)
+ // Execute conversion
+ const inputDir = 'dist/assets/images/social-preview-images/';
+ const outputDir = 'dist/assets/images/social-preview-images/';
+ await convertSvgToJpeg(inputDir, outputDir);
});
+ }
// https://www.11ty.dev/docs/permalinks/#remove-trailing-slashes
// Dropping these normalizes the URls between sitemap.xml and canonical, which is important for indexing.
diff --git a/package-lock.json b/package-lock.json
index 77783091..0fd04426 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,7 +10,9 @@
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
- "luxon": "^3.5.0"
+ "dotenv": "^16.4.7",
+ "luxon": "^3.5.0",
+ "marked": "^15.0.4"
},
"devDependencies": {
"@11ty/eleventy": "^2.0.1",
@@ -2488,6 +2490,17 @@
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
+ "node_modules/dotenv": {
+ "version": "16.4.7",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
+ "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
"node_modules/duplexer": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz",
@@ -4336,6 +4349,17 @@
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
},
+ "node_modules/marked": {
+ "version": "15.0.4",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.4.tgz",
+ "integrity": "sha512-TCHvDqmb3ZJ4PWG7VEGVgtefA5/euFmsIhxtD0XsBxI39gUSKL81mIRFdt0AiNQozUahd4ke98ZdirExd/vSEw==",
+ "bin": {
+ "marked": "bin/marked.js"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
"node_modules/maximatch": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/maximatch/-/maximatch-0.1.0.tgz",
@@ -9260,6 +9284,11 @@
"domhandler": "^4.2.0"
}
},
+ "dotenv": {
+ "version": "16.4.7",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
+ "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ=="
+ },
"duplexer": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz",
@@ -10589,6 +10618,11 @@
}
}
},
+ "marked": {
+ "version": "15.0.4",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.4.tgz",
+ "integrity": "sha512-TCHvDqmb3ZJ4PWG7VEGVgtefA5/euFmsIhxtD0XsBxI39gUSKL81mIRFdt0AiNQozUahd4ke98ZdirExd/vSEw=="
+ },
"maximatch": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/maximatch/-/maximatch-0.1.0.tgz",
diff --git a/package.json b/package.json
index a974594c..76c6684d 100644
--- a/package.json
+++ b/package.json
@@ -62,6 +62,8 @@
"defaults"
],
"dependencies": {
- "luxon": "^3.5.0"
+ "dotenv": "^16.4.7",
+ "luxon": "^3.5.0",
+ "marked": "^15.0.4"
}
}
diff --git a/sent-campaigns.json b/sent-campaigns.json
new file mode 100644
index 00000000..2d4e807d
--- /dev/null
+++ b/sent-campaigns.json
@@ -0,0 +1,4 @@
+[
+ "'Workshop: Custom Form Controls with Web Components'",
+ "Fronteers BE meetup bij In The Pocket in Gent"
+]
\ No newline at end of file
diff --git a/utils/filters.js b/utils/filters.js
index 131dc976..4e073e3d 100644
--- a/utils/filters.js
+++ b/utils/filters.js
@@ -48,4 +48,50 @@ module.exports = {
});
},
-};
\ No newline at end of file
+ readablePostDate(dateObj) {
+ return DateTime.fromJSDate(dateObj, {
+ zone: "Europe/Amsterdam",
+ }).setLocale('en').toLocaleString(DateTime.DATE_FULL);
+ },
+
+ postDate(dateObj) {
+ return DateTime.fromJSDate(dateObj, {
+ zone: "Europe/Amsterdam",
+ }).setLocale('en').toISODate();
+ },
+
+ splitlines(input) {
+ const parts = input.split(' ');
+ const lines = parts.reduce(function (prev, current) {
+ if (!prev.length) {
+ return [current];
+ }
+
+ let lastOne = prev[prev.length - 1];
+
+ if (lastOne.length + current.length > 23) {
+ return [...prev, current];
+ }
+
+ prev[prev.length - 1] = lastOne + ' ' + current;
+ return prev;
+ }, []);
+
+ return lines;
+ },
+
+ stringify(data) {
+ return JSON.stringify(data, null, "\t");
+ },
+
+ customSlug(value) {
+ if (!value) return "fallback-title"; // Fallback for empty titles
+ return strToSlug(value).replace(/\s+/g, '-'); // Replace spaces with dashes
+ },
+
+ localizedDate(dateObj, locale = "en") {
+ return DateTime.fromJSDate(dateObj)
+ .setLocale(locale)
+ .toFormat("d LLLL yyyy");
+ }
+};
diff --git a/utils/listmonk.js b/utils/listmonk.js
new file mode 100644
index 00000000..bfae2b7a
--- /dev/null
+++ b/utils/listmonk.js
@@ -0,0 +1,118 @@
+const fs = require('fs');
+const { marked } = require('marked');
+const { JSDOM } = require('jsdom');
+
+const sentCampaignsFile = './sent-campaigns.json';
+
+// Load sent campaigns
+function getSentCampaigns() {
+ if (!fs.existsSync(sentCampaignsFile)) {
+ fs.writeFileSync(sentCampaignsFile, JSON.stringify([])); // Create file if it doesn't exist
+ }
+ return JSON.parse(fs.readFileSync(sentCampaignsFile));
+}
+
+// Save sent campaigns
+function saveSentCampaigns(sentCampaigns) {
+ fs.writeFileSync(sentCampaignsFile, JSON.stringify(sentCampaigns, null, 2));
+}
+
+// Check and send campaign
+async function checkAndSendCampaign(post) {
+ const sentCampaigns = getSentCampaigns();
+
+ if (sentCampaigns.includes(post.title)) {
+ console.log(`Campaign already sent for: ${post.title}`);
+ return; // Skip if already sent
+ }
+
+ console.log(`Creating campaign for: ${post.title}`);
+ await createListmonkCampaign(post); // Send campaign
+ sentCampaigns.push(post.title); // Add to sent list
+ saveSentCampaigns(sentCampaigns); // Update the list
+}
+
+function extractFrontMatter(content) {
+ const match = /---\n([\s\S]*?)\n---/.exec(content); // Extract YAML frontmatter
+ const body = content.replace(/---\n[\s\S]*?\n---/, '').trim(); // Extract body
+
+ const metadata = {};
+ if (match && match[1]) {
+ match[1].split('\n').forEach(line => {
+ const colonIndex = line.indexOf(':'); // Find first colon only
+ if (colonIndex !== -1) {
+ const key = line.substring(0, colonIndex).trim(); // Key before colon
+ const value = line.substring(colonIndex + 1).trim(); // Value after colon
+ metadata[key] = value; // Store in metadata object
+ }
+ });
+ }
+
+ metadata.content = body; // Add body content
+ return metadata;
+}
+
+async function createListmonkCampaign(post) {
+ const url = process.env.LISTMONK_URL; // Replace with your Listmonk URL
+ const apiKey = process.env.LISTMONK_API_KEY; // Replace with your API key
+ const apiUser = process.env.LISTMONK_API_USER;
+ const baseUrl = 'https://www.fronteers.nl'; // Base URL for links
+
+ let htmlContent = marked(post.content);
+
+ // Step 2: Rewrite relative links to full links
+ const dom = new JSDOM(htmlContent); // Create a DOM instance
+ const document = dom.window.document;
+
+ // Process all tags for 'href'
+ document.querySelectorAll('a').forEach(link => {
+ const href = link.getAttribute('href');
+ if (href && !href.startsWith('http')) {
+ link.setAttribute('href', new URL(href, baseUrl).href); // Resolve relative link
+ }
+ });
+
+ // Process all tags for 'src'
+ document.querySelectorAll('img').forEach(img => {
+ const src = img.getAttribute('src');
+ if (src && !src.startsWith('http')) {
+ img.setAttribute('src', new URL(src, baseUrl).href); // Resolve relative image source
+ }
+ });
+
+ // Convert DOM back to HTML
+ htmlContent = dom.serialize();
+
+ const payload = {
+ name: post.title,
+ subject: post.title,
+ content_type: 'html',
+ body: `${post.title}
${htmlContent}
`, // Email content
+ type: 'regular',
+ lists: [1], // Replace with your list ID
+ };
+
+ // console.log('Campaign Payload:', JSON.stringify(payload, null, 2)); // Log the payload
+
+ try {
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `token ${apiUser}:${apiKey}`,
+ },
+ body: JSON.stringify(payload),
+ });
+
+ const data = await response.json();
+ console.log('Campaign Created:', data);
+ } catch (error) {
+ console.error('Error creating campaign:', error);
+ }
+}
+
+// Export functions
+module.exports = {
+ extractFrontMatter,
+ checkAndSendCampaign,
+};