-
Notifications
You must be signed in to change notification settings - Fork 0
Implement blog snippet system and new landing page #1
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
base: main
Are you sure you want to change the base?
Changes from 22 commits
91300fd
d737f6a
db70c24
347fea9
a3a55ed
e5271ed
24924f5
065b9f0
8b863fd
f1a5592
4eae751
9f5f6ab
3d560e9
10242ed
f74f774
5dd1241
6b35f55
4ca5a91
565bf1e
7b74943
1c36fad
447ed49
b32ab4e
e36c150
a8f408a
ca454b8
0a1eee3
92ba828
37bb713
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,9 @@ | ||
| // @ts-check | ||
| import { defineConfig } from 'astro/config'; | ||
|
|
||
| import react from '@astrojs/react'; | ||
|
|
||
| // https://astro.build/config | ||
| export default defineConfig({}); | ||
| export default defineConfig({ | ||
| integrations: [react()] | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| interface BlogPost { | ||
| id: number; | ||
| date: string; | ||
| slug: string; | ||
| title: { | ||
| rendered: string; | ||
| }; | ||
| guid: { | ||
| rendered: string; | ||
| }; | ||
| content: { | ||
| rendered: string; | ||
| }; | ||
| excerpt: { | ||
| rendered: string; | ||
| }; | ||
| link: string; | ||
| author: number; | ||
| jetpack_featured_media_url: string; | ||
|
|
||
| // embedded data | ||
| _embedded?: { | ||
| author?: { | ||
| name: string; | ||
| }[]; | ||
| }; | ||
| } | ||
|
|
||
| export type { BlogPost }; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,90 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { BlogPost } from "../data/types"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import "../../src/styles/mylanding.css"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { preview } from "astro"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function getStaticPaths() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // _embed query param expands related objects like authors | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const res = await fetch("https://akongalabs.com/wp-json/wp/v2/posts?_embed"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const posts: BlogPost[] = await res.json(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return posts.map((post, index) => ({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| params: { slug: post.slug }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| props: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| post, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| prevPost: posts[index + 1] || null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| nextPost: posts[index - 1] || null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+4
to
+18
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Handle pagination and failures in getStaticPaths() to avoid silently missing posts or build breaks WordPress REST defaults to 10 posts per page. As written, older posts won’t get static paths, and any transient network/API error will throw at build time without context. Apply this diff to fetch all pages and add basic error handling: export async function getStaticPaths() {
- // _embed query param expands related objects like authors
- const res = await fetch("https://akongalabs.com/wp-json/wp/v2/posts?_embed");
- const posts: BlogPost[] = await res.json();
-
- return posts.map((post) => ({
- params: { slug: post.slug },
- props: { post },
- }));
+ // Fetch all posts (WP caps per_page at 100)
+ const posts: BlogPost[] = [];
+ let page = 1;
+ const perPage = 100;
+ while (true) {
+ const res = await fetch(
+ `https://akongalabs.com/wp-json/wp/v2/posts?_embed&per_page=${perPage}&page=${page}`
+ );
+ if (!res.ok) {
+ throw new Error(`Failed to fetch posts (page ${page}): ${res.status} ${res.statusText}`);
+ }
+ const batch: BlogPost[] = await res.json();
+ posts.push(...batch);
+ if (batch.length < perPage) break;
+ page++;
+ }
+ return posts.map((post) => ({
+ params: { slug: post.slug },
+ props: { post },
+ }));
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { post, prevPost, nextPost } = Astro.props as { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| post: BlogPost; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| prevPost: BlogPost | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| nextPost: BlogPost | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <html> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <head> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <title>{post.title.rendered}</title> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <meta charset="UTF-8" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </head> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <body> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <article class="blog-post"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div class="blog-body"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div class="first_links_snips"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <a href="/newLanding">Akong'a Labs</a> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <a href="/blogSnippets">Blogs</a> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div class="blog-content"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <h1 set:html={post.title.rendered} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <img src={post.jetpack_featured_media_url} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p class="author-date"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <span class="author">{post._embedded?.author?.[0]?.name}</span> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+44
to
+47
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Render a safe title and harden featured image (a11y + perf)
- <h1 set:html={post.title.rendered} />
- <img src={post.jetpack_featured_media_url} />
+ <h1>{titleText}</h1>
+ {
+ post.jetpack_featured_media_url && (
+ <img
+ src={post.jetpack_featured_media_url}
+ alt={titleText}
+ loading="lazy"
+ decoding="async"
+ />
+ )
+ }Consider adding width/height to reduce CLS if dimensions are known. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <span class="date"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| new Date(post.date).toLocaleDateString("en-US", { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| year: "numeric", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| month: "long", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| day: "numeric", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div set:html={post.content.rendered} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <script | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data-uid="4e5ba9cc5c" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| src="https://akongalabs.kit.com/4e5ba9cc5c/index.js"></script> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <hr /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (prevPost || nextPost) && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <nav class="blog-navigation"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div class="nav-links"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {prevPost && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <a href={`/${prevPost.slug}`}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Previous: <span set:html={prevPost.title.rendered} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </a> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {nextPost && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <a href={`/${nextPost.slug}`}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Next: <span set:html={nextPost.title.rendered} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </a> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </nav> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div class="last_links"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p><a href="https://x.com/adrianmurage">X</a></p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p><a href="https://github.com/AkongaLabs">GitHub</a></p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </article> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </body> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </html> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| --- | ||
| import "../../src/styles/mylanding.css"; | ||
| import type { BlogPost } from "../data/types"; | ||
|
|
||
| const res = await fetch("https://akongalabs.com/wp-json/wp/v2/posts"); | ||
| const posts: BlogPost[] = await res.json(); | ||
| --- | ||
|
Comment on lines
+1
to
+7
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sanitize HTML, avoid unnecessary set:html, and improve heading semantics
Apply these diffs: ---
-import "../../src/styles/mylanding.css";
+import "../styles/mylanding.css";
import type { BlogPost } from "../data/types";
+import sanitizeHtml from "sanitize-html";
-const res = await fetch("https://akongalabs.com/wp-json/wp/v2/posts");
-const posts: BlogPost[] = await res.json();
+const res = await fetch("https://akongalabs.com/wp-json/wp/v2/posts?per_page=10");
+let posts: BlogPost[] = [];
+if (res.ok) {
+ posts = (await res.json()) as BlogPost[];
+} else {
+ console.warn("Failed to fetch posts:", res.status, res.statusText);
+}
---- <p class="bluecolor" set:html={formattedDate}></p>
- <h1 class="blog-title" set:html={post.title.rendered}></h1>
- <div class="para_space" set:html={cleanedExcerpt}></div>
+ <p class="bluecolor">{formattedDate}</p>
+ <h2 class="blog-title">
+ {post.title.rendered.replace(/<[^>]*>/g, "")}
+ </h2>
+ <div
+ class="para_space"
+ set:html={sanitizeHtml(cleanedExcerpt, {
+ allowedTags: ["p", "em", "strong", "a", "code", "pre", "ul", "ol", "li", "span", "br"],
+ allowedAttributes: { a: ["href", "rel", "target"] },
+ allowedSchemes: ["http", "https", "mailto"],
+ })}
+ ></div>Also applies to: 34-38 🤖 Prompt for AI Agents |
||
|
|
||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="utf-8" /> | ||
| <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> | ||
| <meta name="viewport" content="width=device-width" /> | ||
| <title>Charity's landing page 🐼</title> | ||
| </head> | ||
| <body> | ||
| <div class="blogs_container"> | ||
| <div class="first_links"> | ||
| <a href="/newLanding">Akong'a Labs</a> | ||
| <a href="/blogSnippets">Blogs</a> | ||
| </div> | ||
|
|
||
| <div> | ||
| {posts.map((post) => { | ||
| const formattedDate = new Date(post.date).toLocaleDateString("en-US", { | ||
| year: "numeric", | ||
| month: "long", | ||
| day: "numeric", | ||
| }); | ||
| const cleanedExcerpt = post.excerpt.rendered.replace(/\[…\]/g, "..."); | ||
|
|
||
| return ( | ||
| <div class="asnip"> | ||
| <p class="bluecolor" set:html={formattedDate}></p> | ||
| <h1 class="blog-title" set:html={post.title.rendered}></h1> | ||
| <div class="para_space" set:html={cleanedExcerpt}></div> | ||
| <a class="readmore" href={`/${post.slug}`}>Read more →</a> | ||
| </div> | ||
| ); | ||
| })} | ||
| </div> | ||
|
|
||
| <p class="insider">Get insider posts straight in your inbox</p> | ||
|
|
||
| <div class="convertkit-form"> | ||
| <script async data-uid="4e5ba9cc5c" src="https://akongalabs.kit.com/4e5ba9cc5c/index.js"></script> | ||
| </div> | ||
|
|
||
| <div class="last_links"> | ||
| <p><a href="https://x.com/adrianmurage">X</a></p> | ||
| <p><a href="https://github.com/AkongaLabs">GitHub</a></p> | ||
| </div> | ||
| </div> | ||
| </body> | ||
| </html> | ||
| <!-- <script async data-uid="4e5ba9cc5c" src="https://akongalabs.kit.com/4e5ba9cc5c/index.js"></script> --> | ||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,53 @@ | ||||||||||||
| --- | ||||||||||||
| // import "../../public/styles/mylanding.css"; | ||||||||||||
| import "../../src/styles/mylanding.css" | ||||||||||||
| const isDev = import.meta.env.DEV; | ||||||||||||
| const baseUrl = isDev ? "http://localhost:3001" : ""; | ||||||||||||
|
Comment on lines
+4
to
+5
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Remove unused baseUrl variable. The Apply this diff to remove the unused variable: ---
import '../../public/styles/mylanding.css'
-const isDev = import.meta.env.DEV;
-const baseUrl = isDev ? "http://localhost:3001" : "";
---📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||
| --- | ||||||||||||
|
|
||||||||||||
| <html lang="en"> | ||||||||||||
| <head> | ||||||||||||
| <meta charset="utf-8" /> | ||||||||||||
| <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> | ||||||||||||
| <meta | ||||||||||||
| name="viewport" | ||||||||||||
| content="width=device-width, initial-scale=1, minimum-scale=1" | ||||||||||||
| /> | ||||||||||||
| <meta name="generator" content={Astro.generator} /> | ||||||||||||
| <title>Akong’a Labs</title> | ||||||||||||
| </head> | ||||||||||||
| <body> | ||||||||||||
| <div class="container"> | ||||||||||||
| <div class="content"> | ||||||||||||
| <div class="first_links"> | ||||||||||||
| <a href="/newLanding">Akong'a Labs</a> | ||||||||||||
| <a href="/blogSnippets">Blogs</a> | ||||||||||||
| </div> | ||||||||||||
| <div class="about"> | ||||||||||||
| <h2 class="title">Akong’a — The number “1” in the Pokot language.</h2> | ||||||||||||
| <p> | ||||||||||||
| Akonga Labs is the first of a new kind of company, in Kenya, and | ||||||||||||
| Africa. A <s>startup</s> <strong>stayup</strong> that builds a sustainable business first. | ||||||||||||
| </p> | ||||||||||||
| <p> | ||||||||||||
| Prioritizing building <strong>needed products</strong> in <strong>existing</strong> | ||||||||||||
| and <strong>validated markets.</strong> Prioritizing <strong>profit</strong> over “growth at all cost”. | ||||||||||||
| </p> | ||||||||||||
| </div> | ||||||||||||
|
|
||||||||||||
| <div class="last_links"> | ||||||||||||
| <p><a href="https://x.com/adrianmurage">𝕏</a></p> | ||||||||||||
| <p><a href="https://github.com/AkongaLabs">GitHub</a></p> | ||||||||||||
| </div> | ||||||||||||
| </div> | ||||||||||||
| <div class="image_container"> | ||||||||||||
| <img | ||||||||||||
| class="header_image" | ||||||||||||
| src="/images/header-background.webp" | ||||||||||||
| alt="header image" | ||||||||||||
| /> | ||||||||||||
| <div class="image_text">Akong'a Labs</div> | ||||||||||||
| </div> | ||||||||||||
| </div> | ||||||||||||
| </body> | ||||||||||||
| </html> | ||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sanitize injected HTML and avoid set:html where plain text will do (XSS hardening)
Title, content and other WP-rendered fields can contain HTML. Injecting them directly with set:html is a high-impact XSS risk if upstream sanitization ever regresses or plugins add unsafe markup.
Apply these diffs:
Also applies to: 14-15, 29-29, 42-42