Compare commits
No commits in common. "70a8cb8f9f933729db000f5c03817bc7f4e64b30" and "43eec03edd2e6330362cc5605a48a91387eb49d3" have entirely different histories.
70a8cb8f9f
...
43eec03edd
11 changed files with 221 additions and 382 deletions
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 227 KiB |
|
@ -10,10 +10,8 @@ my time at the BBC. I was fortunate to have a really supportive and personable
|
||||||
manager who was great to work with and who took a real interest in my
|
manager who was great to work with and who took a real interest in my
|
||||||
professional development.
|
professional development.
|
||||||
|
|
||||||
<div style="display:flex; flex-direction: row; justify-content: center; margin:
|

|
||||||
1rem 0;">
|
|
||||||
<img src="./img/bbc-leave-gruv-small.png" width="300" />
|
|
||||||
</div>
|
|
||||||
I worked with a variety of stakeholders on applications used internally within
|
I worked with a variety of stakeholders on applications used internally within
|
||||||
the Corporation. I was able to work across the frontend and the backend in a
|
the Corporation. I was able to work across the frontend and the backend in a
|
||||||
"full-stack" capacity, enhancing my understanding of AWS serverless, Docker,
|
"full-stack" capacity, enhancing my understanding of AWS serverless, Docker,
|
||||||
|
|
|
@ -1,136 +0,0 @@
|
||||||
---
|
|
||||||
title: "Self-hosting this site"
|
|
||||||
slug: /self-hosting-this-site/
|
|
||||||
date: 2025-07-21
|
|
||||||
tags: ["self-hosting"]
|
|
||||||
---
|
|
||||||
|
|
||||||
[Previously](https://systemsobscure.blog/posts/how-I-deploy-this-site) this site
|
|
||||||
was deployed as follows. I used [Gatsby.js](https://gatsbyjs.com) to generate a
|
|
||||||
static website from React JavaScript. When changes to the `main` branch were
|
|
||||||
pushed to the remote GitHub repository, a GitHub action would execute, building
|
|
||||||
the source files and transferring them to an S3 bucket. The HTML generated was
|
|
||||||
then served using AWS Cloud Front.
|
|
||||||
|
|
||||||
As I am now trying to self-host as many services as I can, it was time remove
|
|
||||||
the dependency on AWS and GitHub and instead serve the site from my VPS and
|
|
||||||
manage deployment via my
|
|
||||||
[self-hosted Forgejo instance](https://forgejo.systemsobscure.net/thomasabishop).
|
|
||||||
|
|
||||||
As part of this process I went on a bit of a side-quest to rebuild the site
|
|
||||||
without Gatsby.
|
|
||||||
|
|
||||||
When I tried to update to the lastest version of Gatsby I faced several problems
|
|
||||||
mostly due to Gatsby trying to foist server-side rendering on every project. For
|
|
||||||
such a small blog site with next to zero traffic this is unnecessary. Moreover,
|
|
||||||
I realised that I could also do without all the bloat that Gatsby adds via its
|
|
||||||
plugin ecosystem. (Perhaps if frontend bundles weren't so large, SSR wouldn't be
|
|
||||||
needed lol.)
|
|
||||||
|
|
||||||
So I decided to simplify the blog and build it as a [Vite](https://vite.dev)
|
|
||||||
React application that gets its content from a JSON index that is generated via
|
|
||||||
a pre-build script.
|
|
||||||
|
|
||||||
I
|
|
||||||
[wrote a script](https://forgejo.systemsobscure.net/thomasabishop/systems-obscure/src/branch/main/scripts/generate-post-index.js)
|
|
||||||
in JavaScript that loops through all the blog posts in Markdown format from a
|
|
||||||
`/posts` directory at the project root. It creates a JSON array with an entry
|
|
||||||
for each post. For example, this post would be represented as follows:
|
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"slug": "self-hosting-this-site",
|
|
||||||
"title": "Self-hosting this site",
|
|
||||||
"date": "2025-07-22T00:00:00.00Z",
|
|
||||||
"tags": ["self-hosting"],
|
|
||||||
"html": "<p>Previously this site was..."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
To convert the raw markdown to HTML I used
|
|
||||||
[marked](https://github.com/markedjs/marked). I also added the following steps:
|
|
||||||
|
|
||||||
- convert `<code>` tags into syntax highlighted blocks using
|
|
||||||
[shiki](https://shiki.style/)
|
|
||||||
- compress and resize images and then transfer them to the `public/` directory
|
|
||||||
where Vite sources static assets when deployed
|
|
||||||
- find and replace all image `src` attributes in the resulting HTML with the
|
|
||||||
`public/` file path
|
|
||||||
|
|
||||||
When I run `npm run build:posts` this generates the `post-index.json` file and
|
|
||||||
saves it to `/public` so that the React application can read from it when
|
|
||||||
rendering the site content.
|
|
||||||
|
|
||||||
<figure style="margin: 1rem 0">
|
|
||||||
<video controls>
|
|
||||||
<source src="./img/post-index-build-script.webm" type="video/webm">
|
|
||||||
Your browser does not support the video tag.
|
|
||||||
</video>
|
|
||||||
<figcaption>Running the pre-build script to generate the site content.</figcaption>
|
|
||||||
</figure>
|
|
||||||
|
|
||||||
I created a custom hook called `usePosts` that fetches the index and saves it to
|
|
||||||
the session storage to avoid unnecessary network requests. You can see this
|
|
||||||
being invoked in the
|
|
||||||
[template component for blog posts](https://forgejo.systemsobscure.net/thomasabishop/systems-obscure/src/commit/43eec03edd2e6330362cc5605a48a91387eb49d3/src/templates/BlogTemplate.tsx).
|
|
||||||
|
|
||||||
Having rebuilt the site without the dependency on Gatsby, the next step was to
|
|
||||||
deploy it.
|
|
||||||
|
|
||||||
I wanted to retain the "push and deploy" model that I previously achieved via
|
|
||||||
the GitHub Action. This was easy because the syntax for Forgejo Actions is
|
|
||||||
practically identical. You place the YAML declaration in the `.forejo/`
|
|
||||||
directory of the project and Forejo picks it up on each push to the remote.
|
|
||||||
|
|
||||||
Here's the file:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
name: Deploy Blog
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container: node:18
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- run: npm install
|
|
||||||
- run: npm run build:posts
|
|
||||||
- run: npm run build
|
|
||||||
- run: |
|
|
||||||
echo "${{ secrets.SSH_FORGEJO_KEY }}" > /tmp/ssh_key
|
|
||||||
chmod 600 /tmp/ssh_key
|
|
||||||
ssh -i /tmp/ssh_key -o StrictHostKeyChecking=no ${{ vars.VPS_USER }} "bash -c 'rm -rf /var/www/systemsobscure.blog/*'"
|
|
||||||
scp -i /tmp/ssh_key -o StrictHostKeyChecking=no -r dist/* ${{ vars.VPS_USER }}:/var/www/systemsobscure.blog/
|
|
||||||
rm /tmp/ssh_key
|
|
||||||
```
|
|
||||||
|
|
||||||
Before compiling the build I first run the pre-build script to generate the post
|
|
||||||
index. Then I remove the existing source files for the site in `var/www/` over
|
|
||||||
`ssh` and then transfer the new source files using `scp`. (While I run most
|
|
||||||
third-party services on my VPS via Docker, I prefer to keep it simple with my
|
|
||||||
own applications and not use containers.)
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
In order for the site to be served from `var/www/systemsobscure.blog`, I had to
|
|
||||||
give my Docker instance of nginx running on the VPS access to this directory by
|
|
||||||
[adding it to the volume mappings](https://forgejo.systemsobscure.net/thomasabishop/self-host/commit/8c380b71735f278f4309bbd14ad96cb9a29104b7)
|
|
||||||
in the `docker-compose.`
|
|
||||||
|
|
||||||
Then I added an
|
|
||||||
[nginx `.conf` file for the blog](https://forgejo.systemsobscure.net/thomasabishop/self-host/src/branch/main/proxy/nginx/conf.d/systemsobscure.conf).
|
|
||||||
This file specifies the SSL certificate to use and sets `index.html` as the
|
|
||||||
document root. It also adds some default caching and compression, along with
|
|
||||||
security headers.
|
|
||||||
|
|
||||||
Finally I
|
|
||||||
[updated my SSL certificate generation script](https://forgejo.systemsobscure.net/thomasabishop/self-host/commit/61cdbe43c2041d5961ef74416270794eb6fb91c6)
|
|
||||||
to include `systemsobscure.blog`.
|
|
||||||
|
|
||||||
And that's it. My personal website is now self-hosted on my VPS and is
|
|
||||||
automatically deployed via pushes to my self-hosted Git forge. No more GitHub,
|
|
||||||
no more AWS.
|
|
|
@ -15,27 +15,23 @@ const processBlogImages = async () => {
|
||||||
fs.mkdirSync(destDir, { recursive: true })
|
fs.mkdirSync(destDir, { recursive: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Copy all images
|
||||||
const files = fs.readdirSync(srcDir)
|
const files = fs.readdirSync(srcDir)
|
||||||
const imageExtensions = [
|
const imageExtensions = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"]
|
||||||
".jpg",
|
|
||||||
".jpeg",
|
|
||||||
".png",
|
|
||||||
".gif",
|
|
||||||
".webp",
|
|
||||||
".svg",
|
|
||||||
".webm",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
// Use for...of loop instead of forEach to work with async/await
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const ext = path.extname(file).toLowerCase()
|
const ext = path.extname(file).toLowerCase()
|
||||||
if (imageExtensions.includes(ext)) {
|
if (imageExtensions.includes(ext)) {
|
||||||
const inputPath = path.join(srcDir, file) // Define inputPath and outputPath
|
const inputPath = path.join(srcDir, file) // Define inputPath and outputPath
|
||||||
const outputPath = path.join(destDir, file)
|
const outputPath = path.join(destDir, file)
|
||||||
|
|
||||||
if (ext === ".svg" || ext === ".webm") {
|
if (ext === ".svg") {
|
||||||
|
// SVGs just get copied
|
||||||
fs.copyFileSync(inputPath, outputPath)
|
fs.copyFileSync(inputPath, outputPath)
|
||||||
console.info(`📸 Copied ${file}`)
|
console.info(`📸 Copied ${file}`)
|
||||||
} else {
|
} else {
|
||||||
|
// Process other images
|
||||||
await sharp(inputPath)
|
await sharp(inputPath)
|
||||||
.resize(1200, 800, {
|
.resize(1200, 800, {
|
||||||
fit: "inside",
|
fit: "inside",
|
||||||
|
|
|
@ -65,7 +65,7 @@ const Menu = () => {
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const { theme, setTheme } = useTheme()
|
const { theme, setTheme } = useTheme()
|
||||||
return (
|
return (
|
||||||
<header className="w-full h-12 flex items-center justify-center border-b fixed top-0 z-20 bg-sidebar">
|
<header className="w-full h-12 flex items-center justify-center border-b fixed top-0 z-20 bg-background">
|
||||||
<div className="w-full px-0 md:px-4 flex items-center justify-between">
|
<div className="w-full px-0 md:px-4 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button variant="ghost" asChild>
|
<Button variant="ghost" asChild>
|
||||||
|
|
|
@ -13,8 +13,8 @@ import { convertDate } from "@/utils/convertDate"
|
||||||
const PostListing = ({ posts, title, showAllButton }) => {
|
const PostListing = ({ posts, title, showAllButton }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mb-5">
|
<div className="mb-5 ">
|
||||||
<h2 className="scroll-m-20 text-[1.3rem] font-semibold border-b pb-2">
|
<h2 className="scroll-m-20 text-2xl font-semibold lg:text-2xl border-b pb-3">
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
@ -44,7 +44,7 @@ const PostListing = ({ posts, title, showAllButton }) => {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{showAllButton && (
|
{showAllButton && (
|
||||||
<Button asChild variant="secondary" className="w-full mt-4">
|
<Button asChild variant="" className="w-full mt-4">
|
||||||
<Link to="/posts/page/1">View all</Link>
|
<Link to="/posts/page/1">View all</Link>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -2,109 +2,109 @@ import MainTemplate from "@/templates/MainTemplate"
|
||||||
import portrait from "../images/portrait-compressed.jpg"
|
import portrait from "../images/portrait-compressed.jpg"
|
||||||
|
|
||||||
const AboutPage = () => {
|
const AboutPage = () => {
|
||||||
return (
|
return (
|
||||||
<MainTemplate>
|
<MainTemplate>
|
||||||
<div className="mb-5 ">
|
<div className="mb-5 ">
|
||||||
<h2 className="scroll-m-20 text-2xl font-semibold lg:text-2xl border-b pb-3">
|
<h2 className="scroll-m-20 text-2xl font-semibold lg:text-2xl border-b pb-3">
|
||||||
About
|
About
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<figure className="w-full flex flex-col items-center mb-6">
|
<figure className="w-full flex flex-col items-center mb-6">
|
||||||
<img
|
<img
|
||||||
alt="A portrait of the blog author"
|
alt="A portrait of the blog author"
|
||||||
src={portrait}
|
src={portrait}
|
||||||
className="w-0 flex"
|
className="w-0 flex"
|
||||||
/>
|
/>
|
||||||
<figcaption className="text-sm text-muted-foreground mt-3 text-center">
|
<figcaption className="text-sm text-muted-foreground mt-3 text-center">
|
||||||
Pictured with the WITCH computer at the{" "}
|
Pictured with the WITCH computer at the{" "}
|
||||||
<a
|
<a
|
||||||
href="https://www.tnmoc.org/"
|
href="https://www.tnmoc.org/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-primary hover:text-primary/80"
|
className="text-primary hover:text-primary/80"
|
||||||
>
|
>
|
||||||
National Museum of Computing
|
National Museum of Computing
|
||||||
</a>
|
</a>
|
||||||
, Bletchley Park.
|
, Bletchley Park.
|
||||||
</figcaption>
|
</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
<p className="leading-[1.6] [&:not(:first-child)]:mt-6">
|
<p className="leading-[1.5] [&:not(:first-child)]:mt-6">
|
||||||
I'm a self-taught software engineer based on the south coast of England.
|
I'm a self-taught software engineer based on the south coast of England.
|
||||||
This blog is my technical scrapbook. I document the details of my
|
This blog is my technical scrapbook. I document the details of my
|
||||||
technical life so I can have a record of progress when I look back.{" "}
|
technical life so I can have a record of progress when I look back.{" "}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="leading-[1.6] [&:not(:first-child)]:mt-6">
|
<p className="leading-[1.5] [&:not(:first-child)]:mt-6">
|
||||||
I completed a degree in Philosophy at the University of Warwick (2009)
|
I completed a degree in Philosophy at the University of Warwick (2009)
|
||||||
and hold a Postgraduate Certificate of Education (2011).
|
and hold a Postgraduate Certificate of Education (2011).
|
||||||
</p>
|
</p>
|
||||||
<p className="leading-[1.6] [&:not(:first-child)]:mt-6">
|
<p className="leading-[1.5] [&:not(:first-child)]:mt-6">
|
||||||
Currently I work for{" "}
|
Currently I work for{" "}
|
||||||
<a
|
<a
|
||||||
href="https://en.wikipedia.org/wiki/ITV_(TV_network)"
|
href="https://en.wikipedia.org/wiki/ITV_(TV_network)"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="underline decoration-1 hover:text-primary/80 underline-offset-2"
|
className="underline decoration-1 hover:text-primary/80 underline-offset-2"
|
||||||
>
|
>
|
||||||
ITV
|
ITV
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
{""}
|
{""}
|
||||||
as a backend software engineer. Before that, I worked as a full-stack
|
as a backend software engineer. Before that, I worked as a full-stack
|
||||||
engineer at the{" "}
|
engineer at the{" "}
|
||||||
<a
|
<a
|
||||||
href="https://en.wikipedia.org/wiki/BBC"
|
href="https://en.wikipedia.org/wiki/BBC"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="underline decoration-1 hover:text-primary/80 underline-offset-2 font-medium"
|
className="underline decoration-1 hover:text-primary/80 underline-offset-2"
|
||||||
>
|
>
|
||||||
BBC
|
BBC
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
and as a frontend engineer at{" "}
|
and as a frontend engineer at{" "}
|
||||||
<a
|
<a
|
||||||
href="https://www.arria.com/"
|
href="https://www.arria.com/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="underline decoration-1 hover:text-primary/80 underline-offset-2"
|
className="underline decoration-1 hover:text-primary/80 underline-offset-2"
|
||||||
>
|
>
|
||||||
Arria NLG
|
Arria NLG
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
and in several web developer roles. Before software I was a
|
and in several web developer roles. Before software I was a
|
||||||
teacher.{" "}
|
teacher.{" "}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="leading-[1.6] [&:not(:first-child)]:mt-6">
|
<p className="leading-[1.5] [&:not(:first-child)]:mt-6">
|
||||||
Some things I like:
|
Some things I like:
|
||||||
<ul className="pt-2">
|
<ul className="pt-2">
|
||||||
<li className="mb-1">🐶 Staffies and other bull-breeds</li>
|
<li className="mb-1">🐶 Staffies and other bull-breeds</li>
|
||||||
<li className="mb-1">🎼 Classical music (Haydn, Mozart, JSB)</li>
|
<li className="mb-1">🎼 Classical music (Haydn, Mozart, JSB)</li>
|
||||||
<li className="mb-1">🛸 Science fiction </li>
|
<li className="mb-1">🛸 Science fiction </li>
|
||||||
</ul>
|
</ul>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="leading-[1.6] [&:not(:first-child)]:mt-6">
|
<p className="leading-[1.5] [&:not(:first-child)]:mt-6">
|
||||||
Some things I'm interested in:
|
Some things I'm interested in:
|
||||||
<ul className="pt-2">
|
<ul className="pt-2">
|
||||||
<li className="mb-1">🧑💻 Self-hosting and digital resiliance</li>
|
<li className="mb-1">🧑💻 Self-hosting and digital resiliance</li>
|
||||||
<li className="mb-1">🖳 The history of computing and networks</li>
|
<li className="mb-1">🖳 The history of computing and networks</li>
|
||||||
<li className="mb-1">🇮🇪 Irish history and culture</li>
|
<li className="mb-1">🇮🇪 Irish history and culture</li>
|
||||||
<li className="mb-1">☸️ Buddhism</li>
|
<li className="mb-1">☸️ Buddhism</li>
|
||||||
{/*
|
{/*
|
||||||
|
|
||||||
<li className="mb-1">📡 Civil communications infrastructure</li>
|
<li className="mb-1">📡 Civil communications infrastructure</li>
|
||||||
*/}
|
*/}
|
||||||
</ul>
|
</ul>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="leading-[1.6] [&:not(:first-child)]:mt-6">
|
<p className="leading-[1.5] [&:not(:first-child)]:mt-6">
|
||||||
I self-host my own Git forge at{" "}
|
I self-host my own Git forge at{" "}
|
||||||
<a
|
<a
|
||||||
href="https://forgejo.systemsobscure.net/thomasabishop"
|
href="https://forgejo.systemsobscure.net/thomasabishop"
|
||||||
className="underline decoration-1 hover:text-primary/80 underline-offset-2"
|
className="underline decoration-1 hover:text-primary/80 underline-offset-2"
|
||||||
>
|
>
|
||||||
forgejo.systemsobscure.net
|
forgejo.systemsobscure.net
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
rather than use Microsoft GitHub. You can view my personal projects
|
rather than use Microsoft GitHub. You can view my personal projects
|
||||||
there.
|
there.
|
||||||
</p>
|
</p>
|
||||||
</MainTemplate>
|
</MainTemplate>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export { AboutPage }
|
export { AboutPage }
|
||||||
|
|
|
@ -9,25 +9,14 @@ const HomePage = () => {
|
||||||
const { posts } = usePosts()
|
const { posts } = usePosts()
|
||||||
return (
|
return (
|
||||||
<MainTemplate>
|
<MainTemplate>
|
||||||
<div className="mb-7 border border-foreground py-7 px-6 dark:bg-sidebar">
|
<Card className="mb-8 rounded-none">
|
||||||
<h1 className="scroll-m-20 text-left text-2xl font-semibold">
|
|
||||||
Another software engineer with a blog
|
|
||||||
</h1>
|
|
||||||
<p className="leading-[1.7] [&:not(:first-child)]:mt-5">
|
|
||||||
I'm a self-taught software engineer currently working at ITV,
|
|
||||||
previously at the BBC. This blog is a technical scrapbook and digital
|
|
||||||
garden.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{/*
|
|
||||||
<Card className="mb-8 rounded-none">
|
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<h1 className="scroll-m-20 text-left text-2xl font-semibold">
|
<h1 className="scroll-m-20 text-left text-3xl font-semibold">
|
||||||
Another software engineer with a blog!
|
Another software engineer with a blog
|
||||||
</h1>
|
</h1>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<p className="leading-[1.7] [&:not(:first-child)]:mt-7">
|
<p className="leading-[1.5] [&:not(:first-child)]:mt-6">
|
||||||
I'm a self-taught software engineer currently working at ITV,
|
I'm a self-taught software engineer currently working at ITV,
|
||||||
previously at the BBC. This blog is a technical scrapbook and
|
previously at the BBC. This blog is a technical scrapbook and
|
||||||
digital garden.
|
digital garden.
|
||||||
|
@ -39,13 +28,6 @@ const HomePage = () => {
|
||||||
</Button>
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
<Button asChild>
|
|
||||||
<Link to="/about">About</Link>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<PostListing
|
<PostListing
|
||||||
title="Recent posts"
|
title="Recent posts"
|
||||||
|
|
|
@ -4,129 +4,128 @@ import MainTemplate from "@/templates/MainTemplate"
|
||||||
import { useParams, useNavigate } from "react-router"
|
import { useParams, useNavigate } from "react-router"
|
||||||
import { convertDate } from "@/utils/convertDate"
|
import { convertDate } from "@/utils/convertDate"
|
||||||
import {
|
import {
|
||||||
Pagination,
|
Pagination,
|
||||||
PaginationContent,
|
PaginationContent,
|
||||||
PaginationItem,
|
PaginationItem,
|
||||||
PaginationNext,
|
PaginationNext,
|
||||||
PaginationPrevious,
|
PaginationPrevious,
|
||||||
} from "@/components/ui/pagination"
|
} from "@/components/ui/pagination"
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card"
|
} from "@/components/ui/card"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Link } from "react-router"
|
import { Link } from "react-router"
|
||||||
import { usePosts } from "@/hooks/usePosts"
|
import { usePosts } from "@/hooks/usePosts"
|
||||||
|
|
||||||
const PostsPage = () => {
|
const PostsPage = () => {
|
||||||
const { page } = useParams()
|
const { page } = useParams()
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const { posts } = usePosts()
|
const { posts } = usePosts()
|
||||||
const postsPerPage = 8
|
const postsPerPage = 10
|
||||||
|
|
||||||
const currentPage = Number(page) || 1
|
const currentPage = Number(page) || 1
|
||||||
const totalPages = Math.ceil(posts.length / postsPerPage)
|
const totalPages = Math.ceil(posts.length / postsPerPage)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Only redirect if we have posts and the page is definitively invalid
|
if (currentPage < 1 || currentPage > totalPages) {
|
||||||
if (totalPages > 0 && (currentPage < 1 || currentPage > totalPages)) {
|
navigate(`/posts/page/1`, { replace: true })
|
||||||
navigate(`/posts/page/1`, { replace: true })
|
}
|
||||||
}
|
}, [currentPage, totalPages, navigate])
|
||||||
}, [currentPage, totalPages, navigate])
|
|
||||||
|
|
||||||
const currentPosts = useMemo(() => {
|
const currentPosts = useMemo(() => {
|
||||||
const startIndex = (currentPage - 1) * postsPerPage
|
const startIndex = (currentPage - 1) * postsPerPage
|
||||||
const endIndex = startIndex + postsPerPage
|
const endIndex = startIndex + postsPerPage
|
||||||
return posts.slice(startIndex, endIndex)
|
return posts.slice(startIndex, endIndex)
|
||||||
}, [posts, currentPage, postsPerPage])
|
}, [posts, currentPage, postsPerPage])
|
||||||
|
|
||||||
const hasNextPage = currentPage < totalPages
|
const hasNextPage = currentPage < totalPages
|
||||||
const hasPrevPage = currentPage > 1
|
const hasPrevPage = currentPage > 1
|
||||||
|
|
||||||
const goToNextPage = () => {
|
const goToNextPage = () => {
|
||||||
if (hasNextPage) {
|
if (hasNextPage) {
|
||||||
navigate(`/posts/page/${currentPage + 1}`)
|
navigate(`/posts/page/${currentPage + 1}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const goToPrevPage = () => {
|
const goToPrevPage = () => {
|
||||||
if (hasPrevPage) {
|
if (hasPrevPage) {
|
||||||
navigate(`/posts/page/${currentPage - 1}`)
|
navigate(`/posts/page/${currentPage - 1}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainTemplate>
|
<MainTemplate>
|
||||||
<div className="mb-5 ">
|
<div className="mb-5 ">
|
||||||
<h2 className="scroll-m-20 text-2xl font-semibold lg:text-2xl border-b pb-3">
|
<h2 className="scroll-m-20 text-2xl font-semibold lg:text-2xl border-b pb-3">
|
||||||
All posts
|
All posts
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="min-h-[calc(100vh-200px)] flex flex-col">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-1 gap-3 flex-grow">
|
<div className="grid grid-cols-1 md:grid-cols-1 gap-3 flex-grow">
|
||||||
{currentPosts.map((post) => (
|
{currentPosts.map((post) => (
|
||||||
<Link
|
<Link
|
||||||
to={`/posts/${post.slug}`}
|
to={`/posts/${post.slug}`}
|
||||||
key={post.slug}
|
key={post.slug}
|
||||||
className="block no-underline"
|
className="block no-underline"
|
||||||
>
|
>
|
||||||
<Card
|
<Card
|
||||||
key={post.slug}
|
key={post.slug}
|
||||||
className="flex flex-col h-full hover:bg-primary/5 py-4 px-0 rounded-none"
|
className="flex flex-col h-full hover:bg-primary/5 py-4 px-0 rounded-none"
|
||||||
>
|
>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="leading-snug font-semibold">
|
<CardTitle className="leading-snug font-semibold">
|
||||||
{post.title}
|
{post.title}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-sm text-muted-foreground">
|
<CardDescription className="text-sm text-muted-foreground">
|
||||||
<div className="flex justify-between gap-2">
|
<div className="flex justify-between gap-2">
|
||||||
<span className="text-sm">{convertDate(post.date)}</span>
|
<span className="text-sm">{convertDate(post.date)}</span>
|
||||||
<div className="hidden md:block">
|
<div className="hidden md:block">
|
||||||
{post.tags.map((tag, i) => (
|
{post.tags.map((tag, i) => (
|
||||||
<Badge
|
<Badge
|
||||||
className="ml-2 cursor-pointer"
|
className="ml-2 cursor-pointer"
|
||||||
key={i}
|
key={i}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
navigate(`/tags/${tag}`)
|
navigate(`/tags/${tag}`)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tag}
|
{tag}
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</Card>
|
</Card>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<Pagination className="mt-4">
|
<Pagination className="mt-4">
|
||||||
<PaginationContent>
|
<PaginationContent>
|
||||||
<PaginationItem>
|
<PaginationItem>
|
||||||
<PaginationPrevious
|
<PaginationPrevious
|
||||||
className={`select-none ${hasPrevPage ? "cursor-pointer" : "cursor-not-allowed opacity-50"}`}
|
className={`select-none ${hasPrevPage ? "cursor-pointer" : "cursor-not-allowed opacity-50"}`}
|
||||||
onClick={goToPrevPage}
|
onClick={goToPrevPage}
|
||||||
/>
|
/>
|
||||||
</PaginationItem>
|
</PaginationItem>
|
||||||
<PaginationItem>
|
<PaginationItem>
|
||||||
<PaginationNext
|
<PaginationNext
|
||||||
className={`select-none ${hasNextPage ? "cursor-pointer" : "cursor-not-allowed opacity-50"}`}
|
className={`select-none ${hasNextPage ? "cursor-pointer" : "cursor-not-allowed opacity-50"}`}
|
||||||
onClick={goToNextPage}
|
onClick={goToNextPage}
|
||||||
/>
|
/>
|
||||||
</PaginationItem>
|
</PaginationItem>
|
||||||
</PaginationContent>
|
</PaginationContent>
|
||||||
</Pagination>
|
</Pagination>
|
||||||
</div>
|
</div>
|
||||||
</MainTemplate>
|
</MainTemplate>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export { PostsPage }
|
export { PostsPage }
|
||||||
|
|
|
@ -37,10 +37,10 @@ const BlogTemplate = () => {
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="
|
className="
|
||||||
[&>h2]:text-xl [&>h2]:font-semibold [&>h2]:first:mt-0 [&>h2:not(:first-child)]:mt-8
|
[&>h2]:text-xl [&>h2]:border-b [&>h2]:pb-2 [&>h2]:font-semibold [&>h2]:first:mt-0 [&>h2:not(:first-child)]:mt-8
|
||||||
[&>h3]:text-xl [&>h3]:sm:text-1xl [&>h3]:font-semibold [&>h3:not(:first-child)]:mt-5
|
[&>h3]:text-xl [&>h3]:sm:text-1xl [&>h3]:font-semibold [&>h3:not(:first-child)]:mt-5
|
||||||
[&>h4]:text-lg [&>h4]:sm:text-xl [&>h4]:font-semibold [&>h4:not(:first-child)]:mt-4
|
[&>h4]:text-lg [&>h4]:sm:text-xl [&>h4]:font-semibold [&>h4:not(:first-child)]:mt-4
|
||||||
[&>p]:leading-7 [&>p:not(:first-child)]:mt-4
|
[&>p]:leading-7 [&>p:not(:first-child)]:mt-6
|
||||||
[&>p+:is(h1,h2,h3,h4,h5,h6)]:mt-6
|
[&>p+:is(h1,h2,h3,h4,h5,h6)]:mt-6
|
||||||
[&>blockquote]:mt-4 [&>blockquote]:border-l-2 [&>blockquote]:pl-6 [&>blockquote]:text-muted-foreground [&>blockquote]:text-sm
|
[&>blockquote]:mt-4 [&>blockquote]:border-l-2 [&>blockquote]:pl-6 [&>blockquote]:text-muted-foreground [&>blockquote]:text-sm
|
||||||
[&>ul]:my-4 [&>ul]:ml-6 [&>ul]:list-disc [&>ul>li]:mt-2
|
[&>ul]:my-4 [&>ul]:ml-6 [&>ul]:list-disc [&>ul>li]:mt-2
|
||||||
|
@ -57,10 +57,10 @@ const BlogTemplate = () => {
|
||||||
[&>p+pre]:mt-6
|
[&>p+pre]:mt-6
|
||||||
[&>pre+p]:mt-6
|
[&>pre+p]:mt-6
|
||||||
[&>ul+pre]:mt-6
|
[&>ul+pre]:mt-6
|
||||||
[&>li]:leading-[1.6]
|
[&>li]:leading-[1.5]
|
||||||
[&_li_code]:relative [&_li_code]:rounded [&_li_code]:bg-muted [&_li_code]:px-[0.3rem] [&_li_code]:py-[0.2rem] [&_li_code]:font-mono [&_li_code]:text-sm [&_li_code]:font-normal
|
[&_li_code]:relative [&_li_code]:rounded [&_li_code]:bg-muted [&_li_code]:px-[0.3rem] [&_li_code]:py-[0.2rem] [&_li_code]:font-mono [&_li_code]:text-sm [&_li_code]:font-normal
|
||||||
[&>p]:mt-6 [&>p]:leading-[1.6]
|
[&>p]:mt-6 [&>p]:leading-[1.5]
|
||||||
[&_a]:underline [&_a]:underline-offset-4 [&_a]:hover:text-muted-foreground [&_a]:font-medium
|
[&_a]:underline [&_a]:underline-offset-4 [&_a]:hover:text-muted-foreground
|
||||||
[&>figure]:w-full [&>figure]:flex [&>figure]:flex-col [&>figure]:items-center [&>figure]:justify-center [&>figure]:mb-6 [&>figure]:mx-auto
|
[&>figure]:w-full [&>figure]:flex [&>figure]:flex-col [&>figure]:items-center [&>figure]:justify-center [&>figure]:mb-6 [&>figure]:mx-auto
|
||||||
[&>figure>img]:w-full
|
[&>figure>img]:w-full
|
||||||
[&>figure>figcaption]:text-sm [&>figure>figcaption]:text-muted-foreground [&>figure>figcaption]:mt-3 [&>figure>figcaption]:text-center
|
[&>figure>figcaption]:text-sm [&>figure>figcaption]:text-muted-foreground [&>figure>figcaption]:mt-3 [&>figure>figcaption]:text-center
|
||||||
|
|
Loading…
Add table
Reference in a new issue