diff --git a/posts/2018/2018-03-27-New-Blog.md b/posts/2018/2018-03-27-New-Blog.md new file mode 100644 index 0000000..f638453 --- /dev/null +++ b/posts/2018/2018-03-27-New-Blog.md @@ -0,0 +1,10 @@ +--- +title: New Blog +updated: 2018-03-27 23:39 +tags: blogging +--- + +# New blog +Hallo sebelumnya blog template ini saya pakai untuk domain [callmejeff.me](https://callmejeff.me) dan karena domain akan expired jadi saya pakai kembali untuk domain [arcestia.com](https://arcestia.com). + +Semoga blog ini gak mati kayak yang sebelumnya. \ No newline at end of file diff --git a/posts/2018/2018-04-12-Startup-gagal-lagi.md b/posts/2018/2018-04-12-Startup-gagal-lagi.md new file mode 100644 index 0000000..cbfdf23 --- /dev/null +++ b/posts/2018/2018-04-12-Startup-gagal-lagi.md @@ -0,0 +1,10 @@ +--- +title: Startup Gagal? Kenapa? +updated: 2018-04-12 01:50 +tags: startup,life-story +--- + +# Startup Gagal? +Kegagalan dalam membuat startup bukan yang pertama kalinya bagi saya, untuk kedua kalinya gagal adalah hal yang membuat saya akan berpikir lebih keras lagi kedepannya. Problematika dalam awal-awal membentuk startup sangatlah keras, tak bisa dipungkiri banyak yang berguguran di tahun pertama. Pesaingan yang keras, Berkompetisi untuk mendapatkan seed funding (dana awal). Maupun setelahnya dengan angel investor hingga ke pra seri A ataupun seri A di tahun ke 2 atau ke 3. Selain itu sebuah Tim di dalam startup dituntut untuk kreatif dan berinovasi terus menurus agar apa yang dikerjakan terus bisa berkembang seiringnya perubahan kebutuhan dan minat di masyarakat atau pasar itu sendiri. + +Tidak ada kata terlalu mudah untuk berkarya, selama ada waktu gunakan terus untuk berkarya. diff --git a/posts/2019/2019-05-10-new-chapter-begin-now.md b/posts/2019/2019-05-10-new-chapter-begin-now.md new file mode 100644 index 0000000..4cebdb8 --- /dev/null +++ b/posts/2019/2019-05-10-new-chapter-begin-now.md @@ -0,0 +1,30 @@ +--- +title: New Chapter Begin Now! +updated: 2019-05-10 02:20 +tags: life-story +--- + +# Hidup Baru, Manusia Baru! + +Rabu, 8 Mei 2019 aku mendapatkan kabar bahwa Hasil Ujian A2 untuk Bahasa Jerman di Goethe Institute ku tertanya "Nicht Verstanden" alias tidak lulus. Itu artinya proses pembelajaranku selama 9 bulan ini ada yang tidak beres. Aku harus bisa berubah sepenuhnya, tidak hanya dari 1 atau 2 aspek saja. Tetapi secara total, mulai dari cara hidup, pola aktifitas, hingga metode pembelajaran. + +Beberapa tahun lalu saya pernah buat sebuah paper tentang metode efektifitas metode belajar sesuai dengan kepribadian seseorang. Karena ada orang yang mudah belajar secara visual, ada juga Kinestetik maupun secara auditory. Dan juga ada beberapa teknik yang digunakan sesuai dengan 3 golongan tersebut, oke tapi gak usah bahas itu. (btw ternyata paper itu unpublished, nanti kalau sempat di update lagi.) + +## Why now? + +Semua ini dilatar belakangi oleh beberapa kejadian selama minggu ini. Salah satunya diatas, tapi paling semua bermula dari Senin, 6 Mei 2019. Diriku kaget terbangun karena ada suara marah-marah dan nangis diluar. Ketika keluar saya mendapati Tante ku bicara sambil nada marah dan nangis ke Nenekku. Pendek kata akhirnya dia menceritakan tentang masalah dia dengan suami dan keluarganya.. + +Singkat kata aku pernah publish semua yang diceritakan diweb lain dengan nama pena yang anonymouslah pokoknya. Intinya aja dia pokoknya dimanfaatkan oleh suaminya dan keluarga suaminya. Intinya keluarga ini mau hidup enaknya. Foya-foya terus padahal duit gak ada, ya mereka tinggal ngutang bayarpun kagak. Dst.. + +Hari ini, 10 Mei 2019 nanti pagi sekitar jam 10 aku akan menemani Tanteku untuk kesana menjemput kedua Anaknya. Dan inilah kenapa semua kehidupanku akan berubah dan dirumah akan jadi banyak orang. Artinya ada perubahan dan penyesuaian lagi. + +## A New Chapter Begin. + +Kejadian diatas mendorongku bahwa Tuhan sungguh baik sekali kepadaku, Ia selalu mau menolong diriku Hambanya yang hina dan tak berdaya ini. Siapakah aku? Tapi akhirnya aku mengerti bahwa ini adalah kesempatan bahwa aku harus bisa buktikan aku layak, aku pantas. Pantas dan siap diberikan kekuasaan lebih lagi dari yang sekarang oleh Tuhan.. + +walah.. jadi terlalu emosional begini deh. Tapi pada akhirnya kita harus selalu bergerak maju bukan mundur. Walaupun Jalan yang kita lewati itu gelap tetaplah percaya akan ada Terang diujung nanti. Yuk gunakan tagar #GoingForward #LetsMoveOn dan Jangan Lupa #IndonesiaMenulis. Ayo mulai ceritakan suaramu melalui tulisan-tulisan karena menulis itu menyenangkan loh. + +Jumat, 10 Mei 2019
+Laurensius Jeffrey + +Salam tulis #IndonesiaMenulis \ No newline at end of file diff --git a/posts/2020/2020-10-01-Adding-comments-via-utterances.md b/posts/2020/2020-10-01-Adding-comments-via-utterances.md new file mode 100644 index 0000000..4c4fc52 --- /dev/null +++ b/posts/2020/2020-10-01-Adding-comments-via-utterances.md @@ -0,0 +1,12 @@ +--- +title: Adding Comments (via Utterances) +updated: 2020-10-01 00:00 +tags: blogging,features +--- + + +I've gone back and forth about whether or not to have comments on the site. Most of all, I've liked having absolutely no server or external scripts on the site, and not having to moderate comments that are publicly facing on the site. + +I couldn't keep ignoring how awesome [Utterances](https://utteranc.es/ "Utterances") looks, an [open source project](https://github.com/utterance/utterances "open source project") by [Jeremy Danyow](https://github.com/jdanyow "Jeremy Danyow"). Not only does it look absolutely fantastic, but it took me about 15 minutes to set up completely on my Gatsby site. + +I made a [comments](https://github.com/arcestia/comments "comments") repo to host all the issues and responses. Connecting to the Utterances GitHub app is required to leave a comment through the site, though you can also comment directly through GitHub if the issue exists. I imagine spam and obnoxious comments will be much more rare through this system, and people will actually be able to participate in discussions and leave helpful comments once again. diff --git a/posts/2023/2023-12-14-My-New-Adventure-Will-Begin-in-2024.md b/posts/2023/2023-12-14-My-New-Adventure-Will-Begin-in-2024.md new file mode 100644 index 0000000..865998e --- /dev/null +++ b/posts/2023/2023-12-14-My-New-Adventure-Will-Begin-in-2024.md @@ -0,0 +1,15 @@ +--- +title: My New Adventure Will Begin in 2024 +updated: 2023-12-14 00:00 +tags: blogging,life-story +--- + +As the new year approaches, I am thrilled to embark on a significant new chapter in my life: "My New Adventure Will Begin in 2024". This isn't just a change in the calendar; it's a transformative journey, a leap into a realm of new possibilities and experiences. The year 2024 marks the beginning of a journey that is not just about exploring the world but also about personal growth and pushing boundaries. + +One key aspect of this adventure is the integration of my personal brand, Skiddle.id. This brand represents my passion for creativity, innovation, and connecting with people on a deeper level. Skiddle.id is not just a brand; it's a reflection of my philosophy and approach to life. Through Skiddle.id, I aim to share insights, stories, and experiences that resonate with and inspire others. + +As I document this journey, I'll be sharing the highs and lows, the learnings, and the unforgettable moments through the lens of Skiddle.id. This adventure is an opportunity to not just explore new horizons but also to showcase what Skiddle.id stands for – creativity, adventure, and personal development. + +I invite you to join me in this journey, to share your thoughts, experiences, and insights. Your contributions are not just valuable but essential in shaping this adventure. Together, let's make this journey one of discovery, inspiration, and growth. + +Here's to a new adventure, a year of endless possibilities, and the exciting journey that 2024 promises to be, all under the banner of Skiddle.id! \ No newline at end of file diff --git a/posts/2024/2024-02-21-Guides-To-Git-For-Beginner.md b/posts/2024/2024-02-21-Guides-To-Git-For-Beginner.md new file mode 100644 index 0000000..babf2f6 --- /dev/null +++ b/posts/2024/2024-02-21-Guides-To-Git-For-Beginner.md @@ -0,0 +1,45 @@ +--- +title: Guides To Git For Beginner +updated: 2024-02-21 00:00 +tags: guides +--- + +## Introduction + +Welcome to the beginner's guide to Git! In this guide, we will cover the basics of Git and how to get started with version control in your projects. + +## What is Git? + +Git is a distributed version control system that allows multiple people to collaborate on a project while keeping track of changes made to the codebase. It provides a way to manage and track different versions of your code, making it easier to work on projects with others and revert to previous versions if needed. + +## Installation + +To get started with Git, you'll need to install it on your machine. Here are the steps to install Git: + +1. Visit the official Git website at [https://git-scm.com/](https://git-scm.com/) and download the appropriate version for your operating system. +2. Run the installer and follow the on-screen instructions to complete the installation process. +3. Once the installation is complete, open a terminal or command prompt and type `git --version` to verify that Git has been installed successfully. + +## Basic Git Commands + +Git provides a set of commands that you can use to interact with your code repository. Here are some of the basic Git commands you'll need to know: + +- `git init`: Initializes a new Git repository in your project directory. +- `git add `: Adds a file to the staging area, ready to be committed. +- `git commit -m ""`: Commits the changes in the staging area to the repository with a descriptive message. +- `git status`: Shows the current status of your repository, including any changes that have been made. +- `git log`: Displays a log of all the commits that have been made in the repository. + +## Branching and Merging + +One of the powerful features of Git is the ability to create branches, which allow you to work on different features or bug fixes without affecting the main codebase. Here's how you can create and merge branches in Git: + +- `git branch `: Creates a new branch with the specified name. +- `git checkout `: Switches to the specified branch. +- `git merge `: Merges the changes from the specified branch into the current branch. + +## Conclusion + +Congratulations! You now have a basic understanding of Git and how to get started with version control in your projects. Remember to practice using Git regularly to become more comfortable with its commands and workflows. + +Happy coding! \ No newline at end of file diff --git a/src/app/components/link.tsx b/src/app/components/link.tsx index 1cc57c1..213d039 100644 --- a/src/app/components/link.tsx +++ b/src/app/components/link.tsx @@ -8,7 +8,7 @@ export function Link({ children: React.ReactNode }) { return ( - + {children} ) diff --git a/src/app/root.tsx b/src/app/root.tsx index 7e8955c..19c1156 100644 --- a/src/app/root.tsx +++ b/src/app/root.tsx @@ -17,12 +17,14 @@ import { faRss } from '@fortawesome/free-solid-svg-icons' import tailwindStyles from './tailwind.css' import themeStyles from './styles/theme.css' import homeStyles from './styles/home.css' +import blogStyles from './styles/blog.css' import { ThemeSwitcher } from './components/theme-switcher' export const links: LinksFunction = () => [ { rel: 'stylesheet', href: tailwindStyles }, { rel: 'stylesheet', href: themeStyles }, { rel: 'stylesheet', href: homeStyles }, + { rel: 'stylesheet', href: blogStyles }, { rel: 'preconnect', href: 'https://fonts.googleapis.com' }, { rel: 'preconnect', href: 'https://fonts.gstatic.com', crossOrigin: 'anonymous' }, { @@ -86,6 +88,24 @@ export default function App() {
+
+
+
+ © {new Date().getFullYear()} Laurensius Jeffrey Chandra + + + Built with{' '} + + React + + {' '}and{' '} + + AT Protocol + + +
+
+
diff --git a/src/app/routes/_index.tsx b/src/app/routes/_index.tsx index 0b4b895..6a1a526 100644 --- a/src/app/routes/_index.tsx +++ b/src/app/routes/_index.tsx @@ -3,21 +3,25 @@ import {json, MetaFunction} from '@remix-run/node' import {useLoaderData, NavLink, Link} from '@remix-run/react' import {getProfile} from '../../atproto' import {AppBskyActorDefs} from '@atproto/api' -import { ThemeSwitcher } from '../components/theme-switcher'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faBluesky, faGithub, faMedium } from '@fortawesome/free-brands-svg-icons'; -import { faRss } from '@fortawesome/free-solid-svg-icons'; -import { socialLinks } from '../../data/social-links'; -import { projects } from '../../data/projects'; -import { getPosts } from '../../atproto'; -import { externalLinks } from '../../data/external-links'; -import { fetchMediumFeed } from '../../utils/fetchMediumFeed'; -import { TypingText } from '../components/typing-text'; +import { ThemeSwitcher } from '../components/theme-switcher' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faBluesky, faGithub, faMedium } from '@fortawesome/free-brands-svg-icons' +import { faRss } from '@fortawesome/free-solid-svg-icons' +import { socialLinks } from '../../data/social-links' +import { projects } from '../../data/projects' +import { getPosts } from '../../atproto' +import { externalLinks } from '../../data/external-links' +import { fetchMediumFeed } from '../../utils/fetchMediumFeed' +import { TypingText } from '../components/typing-text' +import { getLocalPosts } from '../../utils/getLocalPosts' export const loader = async () => { - const profile = await getProfile() - const posts = await getPosts(undefined); - const mediumLinks = await fetchMediumFeed(); + const [profile, posts, mediumLinks, localPosts] = await Promise.all([ + getProfile(), + getPosts(undefined), + fetchMediumFeed(), + getLocalPosts() + ]) // Filter out draft posts const postsFiltered = posts @@ -28,24 +32,29 @@ export const loader = async () => { url: `/posts/${post.rkey}`, date: post.createdAt, rkey: post.rkey, - })); + })) // Combine all writing items and sort by date const allItems = [ ...postsFiltered, + ...localPosts.map(post => ({ + ...post, + url: `/blog/${post.year}/${post.slug}`, + type: 'local' + })), ...externalLinks, ...mediumLinks - ].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); + ].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) // Get only the latest 5 items - const latestWritings = allItems.slice(0, 5); + const latestWritings = allItems.slice(0, 5) return json({ profile, latestWritings }) } export const meta: MetaFunction = () => { return [ - {title: "It's Skiddle! 👋"}, + {title: "It's Skiddle! "}, { name: 'description', content: 'javascript, ATProto, decentralized social media', @@ -113,6 +122,8 @@ export default function Index() {
{latestWritings.map((writing, index) => { const isNew = index === 0; + const isExternal = writing.type === 'external'; + const isLocal = writing.type === 'local'; const date = new Date(writing.date); const formattedDate = date.toLocaleDateString('en-US', { year: 'numeric', @@ -128,6 +139,9 @@ export default function Index() { {isNew &&
New!
}
+

+ {isExternal ? writing.source : isLocal ? 'Local Blog' : 'Atprotoblog'} +

); })} @@ -193,13 +207,6 @@ export default function Index() { ))} -
-
-

- 2023 Skiddle's Blog. All rights reserved. -

-
-
) diff --git a/src/app/routes/blog.$year.$slug.tsx b/src/app/routes/blog.$year.$slug.tsx new file mode 100644 index 0000000..f1fc8da --- /dev/null +++ b/src/app/routes/blog.$year.$slug.tsx @@ -0,0 +1,153 @@ +import React from 'react' +import { json, LoaderFunctionArgs, MetaFunction } from '@remix-run/node' +import { useLoaderData } from '@remix-run/react' +import ReactMarkdown from 'react-markdown' +import { promises as fs } from 'fs' +import path from 'path' +import { getProfile } from '../../atproto' + +interface BlogPost { + content: string + frontmatter: { + title: string + date: string + description: string + tags?: string + } +} + +export const loader = async ({ params }: LoaderFunctionArgs) => { + const { year, slug } = params + const profile = await getProfile() + + try { + const postsDir = path.join(process.cwd(), 'posts', year) + // Find the file that matches the slug pattern + const files = await fs.readdir(postsDir) + const postFile = files.find(file => { + // Remove date prefix and .md extension to get the slug + const fileSlug = file.replace(/^\d{4}-\d{2}-\d{2}-/, '').replace(/\.md$/, '') + return fileSlug === slug + }) + + if (!postFile) { + throw new Error('Post not found') + } + + const filePath = path.join(postsDir, postFile) + const content = await fs.readFile(filePath, 'utf-8') + + const [, frontmatterStr = '', ...contentParts] = content.split('---') + const frontmatter = frontmatterStr.split('\n').reduce((acc, line) => { + const [key, ...valueParts] = line.split(':') + if (key && valueParts.length > 0) { + acc[key.trim()] = valueParts.join(':').trim() + } + return acc + }, {} as Record) + + const blogPost: BlogPost = { + content: contentParts.join('---'), + frontmatter: { + title: frontmatter.title || slug.replace(/-/g, ' '), + date: frontmatter.date || frontmatter.updated || new Date().toISOString(), + description: frontmatter.description || '', + tags: frontmatter.tags || '', + }, + } + + return json({ post: blogPost, profile }) + } catch (error) { + throw new Response('Not Found', { status: 404 }) + } +} + +export const meta: MetaFunction = ({ data }) => { + if (!data?.post) { + return [ + { title: 'Post Not Found' }, + { description: 'The requested blog post was not found.' }, + ] + } + + return [ + { title: `${data.post.frontmatter.title} | Skiddle's Blog` }, + { description: data.post.frontmatter.description }, + { name: 'og:title', content: data.post.frontmatter.title }, + { name: 'og:description', content: data.post.frontmatter.description }, + ] +} + +const markdownComponents = { + h1: ({ children }: { children: React.ReactNode }) => ( +

{children}

+ ), + h2: ({ children }: { children: React.ReactNode }) => ( +

{children}

+ ), + h3: ({ children }: { children: React.ReactNode }) => ( +

{children}

+ ), + p: ({ children }: { children: React.ReactNode }) => ( +

{children}

+ ), + a: ({ href, children }: { href?: string; children: React.ReactNode }) => ( + + {children} + + ), + ul: ({ children }: { children: React.ReactNode }) => ( +
    {children}
+ ), + ol: ({ children }: { children: React.ReactNode }) => ( +
    {children}
+ ), + li: ({ children }: { children: React.ReactNode }) => ( +
  • {children}
  • + ), + blockquote: ({ children }: { children: React.ReactNode }) => ( +
    + {children} +
    + ), + code: ({ children }: { children: React.ReactNode }) => ( + {children} + ), + pre: ({ children }: { children: React.ReactNode }) => ( +
    +      {children}
    +    
    + ), +} + +export default function BlogPost() { + const { post, profile } = useLoaderData() + + return ( +
    +
    +

    {post.frontmatter.title}

    + + Written by {profile?.displayName} on{' '} + {new Date(post.frontmatter.date).toLocaleDateString(undefined, { + year: 'numeric', + month: 'long', + day: 'numeric', + })} + +
    + +
    +
    + + {post.content} + +
    +
    + ) +} diff --git a/src/app/routes/feed.tsx b/src/app/routes/feed.tsx index b04c575..09832b0 100644 --- a/src/app/routes/feed.tsx +++ b/src/app/routes/feed.tsx @@ -3,6 +3,7 @@ import { getPosts } from '../../atproto' import { getProfile } from '../../atproto' import { externalLinks } from '../../data/external-links'; import { fetchMediumFeed } from '../../utils/fetchMediumFeed'; +import { getLocalPosts } from '../../utils/getLocalPosts'; // Function to escape special characters in XML function escapeXml(unsafe: string): string { @@ -19,8 +20,11 @@ function escapeXml(unsafe: string): string { } export const loader: LoaderFunction = async ({ request }) => { - const posts = await getPosts(undefined) - const profile = await getProfile() + const [posts, profile, localPosts] = await Promise.all([ + getPosts(undefined), + getProfile(), + getLocalPosts(true) // Include content for RSS feed + ]) // Filter out draft posts const postsFiltered = posts.filter(p => !p.content?.startsWith('NOT_LIVE')) @@ -28,31 +32,49 @@ export const loader: LoaderFunction = async ({ request }) => { // Fetch Medium links const mediumLinks = await fetchMediumFeed(); - // Combine blog posts and external links - const allItems = [...postsFiltered, ...externalLinks, ...mediumLinks] + // Combine all items + const allItems = [ + ...postsFiltered.map(post => ({ + type: 'atprotoblog' as const, + title: post.title, + url: `${new URL(request.url).origin}/posts/${post.rkey}`, + date: post.createdAt, + content: post.content, + })), + ...localPosts.map(post => ({ + ...post, + url: `${new URL(request.url).origin}/blog/${post.year}/${post.slug}`, + content: post.content + })), + ...externalLinks, + ...mediumLinks + ].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) // Create RSS feed const rss = ` -${escapeXml(profile.displayName)} -${escapeXml(profile.description || `Personal Blog of ${profile.displayName}`)} -${escapeXml(new URL(request.url).origin)} +${escapeXml(profile.displayName)}'s Blog +${new URL(request.url).origin} +Personal blog and writings +en-us + ${allItems.map(item => ` - -${escapeXml(item.title + (item.type === 'external' ? ` - ${item.source}` : ''))} - -${new Date(item.date || item.createdAt).toUTCString()} -${escapeXml(item.url || `${new URL(request.url).origin}/posts/${item.rkey}`)} -${escapeXml(item.url || `${new URL(request.url).origin}/posts/${item.rkey}`)} -`).join('\n')} + + ${escapeXml(item.title)} + ${item.url} + ${item.url} + ${new Date(item.date).toUTCString()} + ${item.content ? `${escapeXml(item.content)}` : ''} + +`).join('')} ` return new Response(rss, { headers: { 'Content-Type': 'application/xml', - 'Cache-Control': 'public, max-age=3600' - } + 'Cache-Control': 'public, max-age=3600', + }, }) } diff --git a/src/app/routes/writing.tsx b/src/app/routes/writing.tsx index 0237d65..5b2a41d 100644 --- a/src/app/routes/writing.tsx +++ b/src/app/routes/writing.tsx @@ -7,7 +7,8 @@ import { AppBskyActorDefs } from '@atproto/api' import { Link } from '../components/link' import { externalLinks } from '../../data/external-links' import { WritingItem, BlogPost, ExternalLink } from '../../types/links' -import { fetchMediumFeed } from '../../utils/fetchMediumFeed'; +import { fetchMediumFeed } from '../../utils/fetchMediumFeed' +import { getLocalPosts } from '../../utils/getLocalPosts' export const meta: MetaFunction = () => { return [ @@ -20,8 +21,11 @@ export const meta: MetaFunction = () => { } export const loader = async () => { - const posts = await getPosts(undefined) - const profile = await getProfile() + const [posts, profile, localPosts] = await Promise.all([ + getPosts(undefined), + getProfile(), + getLocalPosts() + ]) const postsFiltered = posts .filter(p => !p.content?.startsWith('NOT_LIVE')) @@ -38,17 +42,20 @@ export const loader = async () => { // Combine blog posts and external links, then sort by date const allItems: WritingItem[] = [ ...postsFiltered, + ...localPosts.map(post => ({ + ...post, + url: `/blog/${post.year}/${post.slug}` + })), ...externalLinks, ...mediumLinks ].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) - console.log('All items:', allItems) // Debug log - return json({ items: allItems, profile }) } -function WritingItemCard({ item }: { item: WritingItem }) { +function WritingItemCard({ item, isNew }: { item: WritingItem; isNew?: boolean }) { const isExternal = item.type === 'external' + const isLocal = item.type === 'local' return ( -
    -

    - {new Date(item.date).toLocaleDateString('en-US', { - year: 'numeric', - month: 'short', - day: 'numeric', - })} -

    -
    +
    +

    {item.title}

    - {isExternal ? (item as ExternalLink).source : 'Atprotoblog'} + {isExternal ? (item as ExternalLink).source : isLocal ? 'Local Blog' : 'Atprotoblog'} +

    +
    +
    +

    + {new Date(item.date).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + })} + {isNew &&

    New!
    }

    @@ -93,7 +103,11 @@ export default function Writing() {
    {items.map((item, index) => ( - + ))}
    diff --git a/src/app/styles/blog.css b/src/app/styles/blog.css new file mode 100644 index 0000000..9efcbb3 --- /dev/null +++ b/src/app/styles/blog.css @@ -0,0 +1,66 @@ +.prose { + max-width: 65ch; + color: #374151; +} + +.prose h1 { + font-size: 2.25em; + margin-top: 0; + margin-bottom: 0.8888889em; + line-height: 1.1111111; +} + +.prose h2 { + font-size: 1.5em; + margin-top: 2em; + margin-bottom: 1em; + line-height: 1.3333333; +} + +.prose p { + margin-top: 1.25em; + margin-bottom: 1.25em; + line-height: 1.75; +} + +.prose code { + background-color: #f3f4f6; + padding: 0.2em 0.4em; + border-radius: 0.25em; + font-size: 0.875em; +} + +.prose pre { + background-color: #1f2937; + color: #e5e7eb; + padding: 1em; + border-radius: 0.375em; + overflow-x: auto; +} + +.prose pre code { + background-color: transparent; + padding: 0; + font-size: 0.875em; +} + +.prose ul { + margin-top: 1.25em; + margin-bottom: 1.25em; + list-style-type: disc; + padding-left: 1.625em; +} + +.prose li { + margin-top: 0.5em; + margin-bottom: 0.5em; +} + +.prose strong { + font-weight: 600; + color: #111827; +} + +.prose em { + font-style: italic; +} diff --git a/src/utils/getLocalPosts.ts b/src/utils/getLocalPosts.ts new file mode 100644 index 0000000..ec23b20 --- /dev/null +++ b/src/utils/getLocalPosts.ts @@ -0,0 +1,75 @@ +import { promises as fs } from 'fs'; +import path from 'path'; + +export interface LocalPost { + type: 'local'; + title: string; + date: string; + description: string; + year: string; + slug: string; + content?: string; +} + +export async function getLocalPosts(includeContent = false): Promise { + const postsDir = path.join(process.cwd(), 'posts'); + const allPosts: LocalPost[] = []; + + try { + const yearDirs = await fs.readdir(postsDir); + + for (const yearDir of yearDirs) { + // Skip non-directory items and non-year directories + if (!yearDir.match(/^\d{4}$/)) continue; + + const yearPath = path.join(postsDir, yearDir); + const stat = await fs.stat(yearPath); + if (!stat.isDirectory()) continue; + + const files = await fs.readdir(yearPath); + const markdownFiles = files.filter(file => file.endsWith('.md')); + + const yearPosts = await Promise.all( + markdownFiles.map(async (file) => { + const content = await fs.readFile(path.join(yearPath, file), 'utf-8'); + const [, frontmatterStr = '', ...contentParts] = content.split('---'); + + const frontmatter = frontmatterStr.split('\n').reduce((acc, line) => { + const [key, ...valueParts] = line.split(':'); + if (key && valueParts.length > 0) { + acc[key.trim()] = valueParts.join(':').trim(); + } + return acc; + }, {} as Record); + + // Extract the slug from the filename, removing the date prefix + const slug = file.replace(/^\d{4}-\d{2}-\d{2}-/, '').replace(/\.md$/, ''); + + // Get the date from frontmatter or filename + const dateMatch = file.match(/^(\d{4}-\d{2}-\d{2})/); + const date = frontmatter.date || frontmatter.updated || + (dateMatch ? dateMatch[1] : new Date().toISOString()); + + return { + type: 'local' as const, + title: frontmatter.title || slug.replace(/-/g, ' '), + date, + description: frontmatter.description || '', + year: yearDir, + slug, + content: includeContent ? contentParts.join('---') : undefined, + }; + }) + ); + + allPosts.push(...yearPosts); + } + + return allPosts.sort((a, b) => + new Date(b.date).getTime() - new Date(a.date).getTime() + ); + } catch (error) { + console.error('Error reading local posts:', error); + return []; + } +}