Skip to content

Commit e86d496

Browse files
committed
Added KeepSec blog
1 parent 46b1cd0 commit e86d496

File tree

6 files changed

+167
-5
lines changed

6 files changed

+167
-5
lines changed

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/App.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55
<router-link to="/" class="nav-link">Home</router-link>
66
<router-link to="/projects" class="nav-link">Projects</router-link>
77
<router-link to="/blog" class="nav-link">Blog</router-link>
8+
<router-link to="/blog-ks" class="nav-link">KeepSec Blog</router-link>
89
</nav>
910
</header>
1011
<main>
11-
<router-view/>
12+
<router-view />
1213
</main>
1314
</div>
1415
</template>

src/router/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router'
22
import HomePage from '@/views/HomePage.vue'
33
import MyProjects from '@/views/MyProjects.vue'
44
import MyBlog from '@/views/MyBlog.vue'
5+
import KeepSecBlog from '@/views/KeepSecBlog.vue'
56

67
const router = createRouter({
78
history: createWebHistory(),
@@ -21,6 +22,11 @@ const router = createRouter({
2122
name: 'MyBlog',
2223
component: MyBlog
2324
},
25+
{
26+
path: '/blog-ks',
27+
name: 'KeepSecBlog',
28+
component: KeepSecBlog
29+
},
2430
]
2531
})
2632

src/views/HomePage.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<template>
22
<div class="home-container">
33
<h1>Hi, I'm Jordan Coupal</h1>
4-
<p>I'm a MLOps Engineer and Open Source Developper with experience programming all sort of tools and maintainer of
5-
some projects on GitHub.</p>
4+
<p>I'm a MLOps Engineer, Co-Founder of KeepSec Technologies and Open Source Developper with experience building
5+
multiple tools and solutions mainly in cloud computing, cybersecurtiy and data integration.</p>
66
<h2>My Skillsets</h2>
77
<div class="skills-container">
88
<div class="skill" v-for="skill in skills" :key="skill">

src/views/KeepSecBlog.vue

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
<template>
2+
<div class="blog-container">
3+
<h2>Posts</h2>
4+
<div class="category-filter">
5+
<button v-for="category in allCategories" :key="category" @click="selectedCategory = category"
6+
:class="{ active: selectedCategory === category }">
7+
{{ category }}
8+
</button>
9+
</div>
10+
<br>
11+
<div class="post" v-for="post in filteredPosts" :key="post.guid">
12+
<h3><a :href="post.link" target="_blank" rel="noopener noreferrer">{{ post.title }}</a></h3>
13+
<div v-html="post.shortContent" class="post-content"></div>
14+
<br>
15+
<div class="post-categories">
16+
<span v-for="category in post.categories" :key="category">{{ category }}</span>
17+
</div>
18+
<p class="post-meta">Published on {{ post.formattedPubDate }}</p>
19+
</div>
20+
</div>
21+
</template>
22+
23+
<script>
24+
export default {
25+
data() {
26+
return {
27+
posts: [],
28+
selectedCategory: 'All',
29+
allCategories: [],
30+
};
31+
},
32+
async mounted() {
33+
await this.fetchPosts();
34+
},
35+
methods: {
36+
async fetchPosts() {
37+
// Using my cors-anywhere instance as a CORS proxy
38+
const corsProxyUrl = 'https://cors-anywhere.jcoupal.com/';
39+
const mediumRssUrl = 'https://www.keepsec.ca/blog/rss';
40+
const proxyUrl = `${corsProxyUrl}${mediumRssUrl}`;
41+
try {
42+
const response = await fetch(proxyUrl);
43+
if (!response.ok) {
44+
throw new Error(`Failed to fetch blog posts: ${response.statusText}`);
45+
}
46+
const text = await response.text();
47+
this.posts = this.parseRSSFeed(text);
48+
this.allCategories = ['All', ...new Set(this.posts.flatMap(post => post.categories))];
49+
} catch (error) {
50+
console.error('Error fetching blog posts:', error);
51+
}
52+
},
53+
parseRSSFeed(rssText) {
54+
const parser = new DOMParser();
55+
const xmlDoc = parser.parseFromString(rssText, "application/xml");
56+
const items = xmlDoc.querySelectorAll("item");
57+
const posts = [];
58+
59+
items.forEach(item => {
60+
const title = item.querySelector("title").textContent;
61+
const link = item.querySelector("link").textContent;
62+
const guid = item.querySelector("guid").textContent;
63+
const pubDate = item.querySelector("pubDate").textContent;
64+
const formattedPubDate = this.formatPubDate(pubDate);
65+
const categories = Array.from(item.querySelectorAll("category")).map(c => c.textContent);
66+
67+
const contentEncodedElements = item.getElementsByTagNameNS("http://purl.org/rss/1.0/modules/content/", "encoded");
68+
let contentEncoded = "";
69+
contentEncoded = contentEncodedElements[0].textContent;
70+
const shortContent = this.createShortContent(contentEncoded);
71+
posts.push({ title, link, guid, shortContent, categories, formattedPubDate });
72+
});
73+
return posts;
74+
},
75+
formatPubDate(pubDateStr) {
76+
const date = new Date(pubDateStr);
77+
const options = { day: 'numeric', month: 'short', year: 'numeric' };
78+
return date.toLocaleDateString('en-US', options);
79+
},
80+
createShortContent(htmlContent) {
81+
const div = document.createElement("div");
82+
div.innerHTML = htmlContent;
83+
// Get text content for the first paragraph or a text slice
84+
const textContent = div.querySelector("p") ? div.querySelector("p").textContent.slice(0, 155) + "..." : div.textContent.slice(0, 200) + "...";
85+
return textContent;
86+
},
87+
},
88+
computed: {
89+
filteredPosts() {
90+
if (this.selectedCategory === 'All') {
91+
return this.posts;
92+
}
93+
return this.posts.filter(post => post.categories.includes(this.selectedCategory));
94+
}
95+
},
96+
}
97+
</script>
98+
99+
<style scoped>
100+
.category-filter button {
101+
margin: 2px;
102+
color: #e0e0e0;
103+
background-color: #333;
104+
padding: 1px 5px;
105+
border: none;
106+
border-radius: 5px;
107+
font-size: 16px;
108+
cursor: pointer;
109+
font-family: 'Open Sans', sans-serif;
110+
}
111+
112+
.category-filter button.active,
113+
.category-filter button:hover {
114+
background-color: #4DBA87;
115+
}
116+
117+
.blog-container {
118+
max-width: 800px;
119+
margin: 0 auto;
120+
padding: 20px;
121+
}
122+
123+
.post {
124+
background-color: #1e1e1e;
125+
color: #e0e0e0;
126+
padding: 20px;
127+
margin-bottom: 20px;
128+
border-radius: 8px;
129+
transition: background-color 0.3s ease;
130+
}
131+
132+
.post:hover {
133+
background-color: #292929;
134+
}
135+
136+
.post h3 a {
137+
color: #4DBA87;
138+
text-decoration: none;
139+
}
140+
141+
.post-meta {
142+
margin-bottom: -0px;
143+
}
144+
145+
.post-categories span {
146+
margin: 2px;
147+
background-color: #333;
148+
padding: 1px 5px;
149+
border-radius: 5px;
150+
}
151+
152+
.post-categories span:hover {
153+
background-color: #4DBA87;
154+
}
155+
</style>

src/views/MyBlog.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<template>
22
<div class="blog-container">
3-
<h2>Blog Posts</h2>
3+
<h2>Posts</h2>
44
<div class="category-filter">
55
<button v-for="category in allCategories" :key="category" @click="selectedCategory = category"
66
:class="{ active: selectedCategory === category }">

0 commit comments

Comments
 (0)