diff --git a/markdown/blog/2024-december-and-paris.md b/markdown/blog/2024-december-and-paris.md
new file mode 100644
index 000000000000..e0f7a365bc19
--- /dev/null
+++ b/markdown/blog/2024-december-and-paris.md
@@ -0,0 +1,138 @@
+---
+title: "November and December Community Update And AsyncAPI Conf in Paris 2024"
+date: 2024-12-16T06:00:00+01:00
+type: Communication
+tags:
+ - Project Status
+cover: /img/posts/2024-blog-banner/blog-banner-december.webp
+authors:
+ - name: Thulisile Sibanda
+ photo: /img/avatars/thulieblack.webp
+ link: https://www.linkedin.com/in/v-thulisile-sibanda/
+ byline: Community Builder and Open Source Fanatic!
+excerpt: 'November and December Community Update And Paris Conference Summary'
+featured: true
+---
+I can't believe we are in the final weeks of the year, and it has been an eventful one. As a community, we have experienced both proud and painful moments, celebrated our victories, and faced challenges and losses. However, in the end, we overcame those difficulties and emerged stronger and better.
+
+Although this summary is a bit later than usual, I am excited to share the details of what happened in November and December and the highlights from the last conference of the year, which took place in Paris.
+
+## AsyncAPI Community Building and Maintenance Goals Proposal 2025
+
+The 2025 community building and maintenance goals proposal is currently open for discussion and will soon be put to a vote for TSC members. We would love your thoughts and suggestions, particularly when solving our community's challenges.
+[Please take a moment to review the open PR related to the AsyncAPI Community Building Goals for 2025](https://github.com/asyncapi/community/pull/1575) to participate in the discussion and share your ideas and solutions.
+
+## AsyncAPI Conf in Paris 2024
+
+The AsyncAPI Conf was back again in Paris this December, thanks to [Mehdi Medjaoui](https://www.linkedin.com/in/mehdimedjaoui/) and the amazing team at [APIdays](https://www.apidays.global/) for hosting and sponsoring the venue. We participated in the three-day event, celebrating a year of the `API Standards` booth alongside friends from OpenAPI, JSON Schema, and GraphQL. Special thanks to all the members of the AsyncAPI community who could join the conference and help at the booth: [Hugo Guerrero](https://www.linkedin.com/in/hugoguerrero/), [Fran Méndez](https://www.linkedin.com/in/fmvilas/), [Richard Coppen](https://www.linkedin.com/in/richard-coppen/), [Hari Krishnan](https://www.linkedin.com/in/harikrishnan83/), and [Lukasz Gornicki](https://www.linkedin.com/in/lukasz-gornicki-a621914/).
+
+The AsyncAPI Conf track took place on the 3rd day of the conference, featuring an impressive lineup of sessions that attracted a diverse audience. The event was consistently packed, with attendees engaged throughout the day.
+
+
+
+[Fran Méndez](https://www.linkedin.com/in/fmvilas/) and [Lukasz Gornicki](https://www.linkedin.com/in/lukasz-gornicki-a621914/) started the track with a welcome speech and mentioned that AsyncAPI recently celebrated its 8th anniversary in November.
+
+
+
+[Naresh Jain](https://www.linkedin.com/in/nareshjain/) and [Pierre Gauthier](https://www.linkedin.com/in/pierre-gauthier-46080916/) presented their keynote on `TMForum's AsyncAPI for a New Era of Event-Driven Architecture`. During the session, Pierre announced TMForum adopts AsyncAPI as a standard, with over 120 telco APIs already in production. TMForum has around 800 telco companies, and they will implement all APIs in an async manner, extensively utilizing the request-reply pattern.
+
+
+
+[Frank Kilcommins](https://www.linkedin.com/in/frank-kilcommins) demonstrated how treating API governance as an enabler unlocks the ability to deliver compelling developer experiences for producers and consumers in event-driven architecture (EDA).
+
+
+
+[Leonid Lukyanov](https://www.linkedin.com/in/leonid-lukyanov/) shared how EDAs introduce new data models, protocols, and APIs not found in the traditional REST/CRUD application stack. And how one can abstract these elements to make them feel familiar to application developers, allowing them to create streaming applications seamlessly.
+
+
+
+[Hugo Guerrero](https://www.linkedin.com/in/hugoguerrero/) then shared how the AsyncAPI Initiative is not only in charge of the specification but has created open-source projects to make it easier for developers to work with the specification documents.
+
+
+
+[Julien Testut](https://www.linkedin.com/in/julientestut/) and [Alessandro Cagnetti](https://www.linkedin.com/in/cagnetti/) shared how organizations can harness the full potential of event-driven integration by leveraging GoldenGate Data Streams, AsyncAPI, and Solace PubSub+ Event Mesh. They shared a great use case for AI and how it can be trained real-time and standardized with AsyncAPI.
+
+
+
+[Annegret Junker](https://www.linkedin.com/in/dr-annegret-junker-141a99a4/) explained how to design effective asynchronous APIs by using an API-first approach. The importance of defining Kafka topics and structuring your definitions.
+
+
+
+
+[Jonathan Michaux](https://www.linkedin.com/in/jmcx/) spoke on how leveraging AI agents with AsyncAPI can create conversational interfaces that dynamically interact with event streams and asynchronous messaging systems.
+
+
+
+[Hari Krishnan](https://www.linkedin.com/in/harikrishnan83) and [Joel Rosario](https://www.linkedin.com/in/joel-c-rosario/) touched on leveraging the AsyncAPI specification as an executable contract and how to isolate and test each component within an EDA.
+
+
+
+[Laurent Broudoux](https://www.linkedin.com/in/laurentbroudoux/) and [Hugo Guerrero](https://www.linkedin.com/in/hugoguerrero/) ended the day by explaining how to use Microcks to provide a solution for mocking and contract-testing your async APIs without extensive coding and empowering you to build extensive and reliable integration tests.
+
+
+
+## Technical Steering Committee
+
+Part of doing mentorships is witnessing the growth within the community, and we are excited to welcome [Ashmit Jagtap](https://www.linkedin.com/in/ashmit-jagtap) as our newest addition to the maintainers list and TSC member. We are proud of the work you have done so far.
+
+
+
+## Final Remarks
+
+It's been a privilege to write the AsyncAPI monthly summary blog consistently. As this is the final blog for the year, I am genuinely grateful for the opportunity to serve and continue supporting the community.
+
+As we approach the holidays, I wish everyone happy holidays and a fantastic 2025.
+
+I'll be back next year with an overall review summary of 2024.
+
+**Until then, stay safe, and happy holidays!**
\ No newline at end of file
diff --git a/markdown/blog/2024-october-summary.md b/markdown/blog/2024-october-summary.md
index 88d9e7b9e36a..1093df77ba1c 100644
--- a/markdown/blog/2024-october-summary.md
+++ b/markdown/blog/2024-october-summary.md
@@ -11,7 +11,6 @@ authors:
link: https://www.linkedin.com/in/v-thulisile-sibanda/
byline: AsyncAPI Community Manager
excerpt: 'October Community Update And Online Conference Summary'
-featured: true
---
October marked the third AsyncAPI Conference, this time an online edition. As someone fortunate enough to be extensively involved in organizing the conferences, I saw the challenges that came with in-person events.
diff --git a/public/img/posts/2024-blog-banner/blog-banner-december.webp b/public/img/posts/2024-blog-banner/blog-banner-december.webp
new file mode 100644
index 000000000000..043e9283a837
Binary files /dev/null and b/public/img/posts/2024-blog-banner/blog-banner-december.webp differ
diff --git a/public/img/posts/paris-conf-2024/annegret.webp b/public/img/posts/paris-conf-2024/annegret.webp
new file mode 100644
index 000000000000..87de55e1fe39
Binary files /dev/null and b/public/img/posts/paris-conf-2024/annegret.webp differ
diff --git a/public/img/posts/paris-conf-2024/frank.webp b/public/img/posts/paris-conf-2024/frank.webp
new file mode 100644
index 000000000000..b9249192f509
Binary files /dev/null and b/public/img/posts/paris-conf-2024/frank.webp differ
diff --git a/public/img/posts/paris-conf-2024/full-room.webp b/public/img/posts/paris-conf-2024/full-room.webp
new file mode 100644
index 000000000000..590c3c880206
Binary files /dev/null and b/public/img/posts/paris-conf-2024/full-room.webp differ
diff --git a/public/img/posts/paris-conf-2024/hari-and-joel.webp b/public/img/posts/paris-conf-2024/hari-and-joel.webp
new file mode 100644
index 000000000000..20cad1725c12
Binary files /dev/null and b/public/img/posts/paris-conf-2024/hari-and-joel.webp differ
diff --git a/public/img/posts/paris-conf-2024/hugo.webp b/public/img/posts/paris-conf-2024/hugo.webp
new file mode 100644
index 000000000000..6f34ad741f84
Binary files /dev/null and b/public/img/posts/paris-conf-2024/hugo.webp differ
diff --git a/public/img/posts/paris-conf-2024/jonathan.webp b/public/img/posts/paris-conf-2024/jonathan.webp
new file mode 100644
index 000000000000..d84c181c896e
Binary files /dev/null and b/public/img/posts/paris-conf-2024/jonathan.webp differ
diff --git a/public/img/posts/paris-conf-2024/julien-and-alessandro.webp b/public/img/posts/paris-conf-2024/julien-and-alessandro.webp
new file mode 100644
index 000000000000..c93b90a6014a
Binary files /dev/null and b/public/img/posts/paris-conf-2024/julien-and-alessandro.webp differ
diff --git a/public/img/posts/paris-conf-2024/laurent-and-hugo.webp b/public/img/posts/paris-conf-2024/laurent-and-hugo.webp
new file mode 100644
index 000000000000..318a2f81ccd1
Binary files /dev/null and b/public/img/posts/paris-conf-2024/laurent-and-hugo.webp differ
diff --git a/public/img/posts/paris-conf-2024/leonid.webp b/public/img/posts/paris-conf-2024/leonid.webp
new file mode 100644
index 000000000000..87f11a883aa1
Binary files /dev/null and b/public/img/posts/paris-conf-2024/leonid.webp differ
diff --git a/public/img/posts/paris-conf-2024/lukasz-and-fran.webp b/public/img/posts/paris-conf-2024/lukasz-and-fran.webp
new file mode 100644
index 000000000000..9417295f9bc9
Binary files /dev/null and b/public/img/posts/paris-conf-2024/lukasz-and-fran.webp differ
diff --git a/public/img/posts/paris-conf-2024/naresh-and-pierre.webp b/public/img/posts/paris-conf-2024/naresh-and-pierre.webp
new file mode 100644
index 000000000000..3850ded92269
Binary files /dev/null and b/public/img/posts/paris-conf-2024/naresh-and-pierre.webp differ
diff --git a/scripts/markdown/check-markdown.js b/scripts/markdown/check-markdown.js
index 8979f7e0b4ab..cd3bd7ddd1c5 100644
--- a/scripts/markdown/check-markdown.js
+++ b/scripts/markdown/check-markdown.js
@@ -1,4 +1,4 @@
-const fs = require('fs');
+const fs = require('fs').promises;
const matter = require('gray-matter');
const path = require('path');
@@ -98,14 +98,10 @@ function validateDocs(frontmatter) {
* @param {Function} validateFunction - The function used to validate the frontmatter.
* @param {string} [relativePath=''] - The relative path of the folder for logging purposes.
*/
-function checkMarkdownFiles(folderPath, validateFunction, relativePath = '') {
- fs.readdir(folderPath, (err, files) => {
- if (err) {
- console.error('Error reading directory:', err);
- return;
- }
-
- files.forEach(file => {
+async function checkMarkdownFiles(folderPath, validateFunction, relativePath = '') {
+ try {
+ const files = await fs.readdir(folderPath);
+ const filePromises = files.map(async (file) => {
const filePath = path.join(folderPath, file);
const relativeFilePath = path.join(relativePath, file);
@@ -114,17 +110,13 @@ function checkMarkdownFiles(folderPath, validateFunction, relativePath = '') {
return;
}
- fs.stat(filePath, (err, stats) => {
- if (err) {
- console.error('Error reading file stats:', err);
- return;
- }
+ const stats = await fs.stat(filePath);
// Recurse if directory, otherwise validate markdown file
if (stats.isDirectory()) {
- checkMarkdownFiles(filePath, validateFunction, relativeFilePath);
+ await checkMarkdownFiles(filePath, validateFunction, relativeFilePath);
} else if (path.extname(file) === '.md') {
- const fileContent = fs.readFileSync(filePath, 'utf-8');
+ const fileContent = await fs.readFile(filePath, 'utf-8');
const { data: frontmatter } = matter(fileContent);
const errors = validateFunction(frontmatter);
@@ -134,13 +126,33 @@ function checkMarkdownFiles(folderPath, validateFunction, relativePath = '') {
process.exitCode = 1;
}
}
- });
});
- });
+
+ await Promise.all(filePromises);
+ } catch (err) {
+ console.error(`Error in directory ${folderPath}:`, err);
+ throw err;
+ }
}
const docsFolderPath = path.resolve(__dirname, '../../markdown/docs');
const blogsFolderPath = path.resolve(__dirname, '../../markdown/blog');
-checkMarkdownFiles(docsFolderPath, validateDocs);
-checkMarkdownFiles(blogsFolderPath, validateBlogs);
+async function main() {
+ try {
+ await Promise.all([
+ checkMarkdownFiles(docsFolderPath, validateDocs),
+ checkMarkdownFiles(blogsFolderPath, validateBlogs)
+ ]);
+ } catch (error) {
+ console.error('Failed to validate markdown files:', error);
+ process.exit(1);
+ }
+}
+
+/* istanbul ignore next */
+if (require.main === module) {
+ main();
+}
+
+module.exports = { validateBlogs, validateDocs, checkMarkdownFiles, main, isValidURL };
diff --git a/tests/markdown/check-markdown.test.js b/tests/markdown/check-markdown.test.js
new file mode 100644
index 000000000000..85e06b70383f
--- /dev/null
+++ b/tests/markdown/check-markdown.test.js
@@ -0,0 +1,150 @@
+const fs = require('fs').promises;
+const path = require('path');
+const os = require('os');
+const {
+ isValidURL,
+ main,
+ validateBlogs,
+ validateDocs,
+ checkMarkdownFiles
+} = require('../../scripts/markdown/check-markdown');
+
+describe('Frontmatter Validator', () => {
+ let tempDir;
+ let mockConsoleError;
+ let mockProcessExit;
+
+ beforeEach(async () => {
+ mockConsoleError = jest.spyOn(console, 'error').mockImplementation();
+ mockProcessExit = jest.spyOn(process, 'exit').mockImplementation();
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'test-config'));
+ });
+
+ afterEach(async () => {
+ mockConsoleError.mockRestore();
+ mockProcessExit.mockRestore();
+ await fs.rm(tempDir, { recursive: true, force: true });
+ });
+
+ it('validates authors array and returns specific errors', async () => {
+ const frontmatter = {
+ title: 'Test Blog',
+ date: '2024-01-01',
+ type: 'blog',
+ tags: ['test'],
+ cover: 'cover.jpg',
+ authors: [{ name: 'John' }, { photo: 'jane.jpg' }, { name: 'Bob', photo: 'bob.jpg', link: 'not-a-url' }]
+ };
+
+ const errors = validateBlogs(frontmatter);
+ expect(errors).toEqual(expect.arrayContaining([
+ 'Author at index 0 is missing a photo',
+ 'Author at index 1 is missing a name',
+ 'Invalid URL for author at index 2: not-a-url'
+ ]));
+ });
+
+ it('validates docs frontmatter for required fields', async () => {
+ const frontmatter = { title: 123, weight: 'not-a-number' };
+ const errors = validateDocs(frontmatter);
+ expect(errors).toEqual(expect.arrayContaining([
+ 'Title is missing or not a string',
+ 'Weight is missing or not a number'
+ ]));
+ });
+
+ it('checks for errors in markdown files in a directory', async () => {
+ await fs.writeFile(path.join(tempDir, 'invalid.md'), `---\ntitle: Invalid Blog\n---`);
+ const mockConsoleLog = jest.spyOn(console, 'log').mockImplementation();
+
+ await checkMarkdownFiles(tempDir, validateBlogs);
+
+ expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('Errors in file invalid.md:'));
+ mockConsoleLog.mockRestore();
+ });
+
+ it('returns multiple validation errors for invalid blog frontmatter', async () => {
+ const frontmatter = {
+ title: 123,
+ date: 'invalid-date',
+ type: 'blog',
+ tags: 'not-an-array',
+ cover: ['not-a-string'],
+ authors: { name: 'John Doe' }
+ };
+ const errors = validateBlogs(frontmatter);
+
+ expect(errors).toEqual([
+ 'Invalid date format: invalid-date',
+ 'Tags should be an array',
+ 'Cover must be a string',
+ 'Authors should be an array']);
+ });
+
+ it('logs error to console when an error occurs in checkMarkdownFiles', async () => {
+ const invalidFolderPath = path.join(tempDir, 'non-existent-folder');
+
+ await expect(checkMarkdownFiles(invalidFolderPath, validateBlogs))
+ .rejects.toThrow('ENOENT');
+
+ expect(mockConsoleError.mock.calls[0][0]).toContain('Error in directory');
+ });
+
+ it('skips the "reference/specification" folder during validation', async () => {
+ const referenceSpecDir = path.join(tempDir, 'reference', 'specification');
+ await fs.mkdir(referenceSpecDir, { recursive: true });
+ await fs.writeFile(path.join(referenceSpecDir, 'skipped.md'), `---\ntitle: Skipped File\n---`);
+
+ const mockConsoleLog = jest.spyOn(console, 'log').mockImplementation();
+
+ await checkMarkdownFiles(tempDir, validateDocs);
+
+ expect(mockConsoleLog).not.toHaveBeenCalledWith(expect.stringContaining('Errors in file reference/specification/skipped.md'));
+ mockConsoleLog.mockRestore();
+ });
+
+ it('logs and rejects when an exception occurs while processing a file', async () => {
+ const filePath = path.join(tempDir, 'invalid.md');
+ await fs.writeFile(filePath, `---\ntitle: Valid Title\n---`);
+
+ const mockReadFile = jest.spyOn(fs, 'readFile').mockRejectedValue(new Error('Test readFile error'));
+
+ await expect(checkMarkdownFiles(tempDir, validateBlogs)).rejects.toThrow('Test readFile error');
+ expect(mockConsoleError).toHaveBeenCalledWith(
+ expect.stringContaining(`Error in directory`),
+ expect.any(Error)
+ );
+
+ mockReadFile.mockRestore();
+ });
+
+ it('should handle main function errors and exit with status 1', async () => {
+ jest.spyOn(fs, 'readdir').mockRejectedValue(new Error('Test error'));
+
+ await main();
+
+ expect(mockProcessExit).toHaveBeenCalledWith(1);
+
+ expect(mockConsoleError).toHaveBeenCalledWith(
+ 'Failed to validate markdown files:',
+ expect.any(Error)
+ );
+ });
+
+ it('should handle successful main function execution', async () => {
+
+ await main();
+
+ expect(mockConsoleError).not.toHaveBeenCalledWith();
+ });
+
+ it('should return true or false for URLs', () => {
+ expect(isValidURL('http://example.com')).toBe(true);
+ expect(isValidURL('https://www.example.com')).toBe(true);
+ expect(isValidURL('ftp://ftp.example.com')).toBe(true);
+ expect(isValidURL('invalid-url')).toBe(false);
+ expect(isValidURL('/path/to/file')).toBe(false);
+ expect(isValidURL('www.example.com')).toBe(false);
+ });
+
+});