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
|
||||
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
|
||||
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,
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
|
||||
// Copy all images
|
||||
const files = fs.readdirSync(srcDir)
|
||||
const imageExtensions = [
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".png",
|
||||
".gif",
|
||||
".webp",
|
||||
".svg",
|
||||
".webm",
|
||||
]
|
||||
const imageExtensions = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"]
|
||||
|
||||
// Use for...of loop instead of forEach to work with async/await
|
||||
for (const file of files) {
|
||||
const ext = path.extname(file).toLowerCase()
|
||||
if (imageExtensions.includes(ext)) {
|
||||
const inputPath = path.join(srcDir, file) // Define inputPath and outputPath
|
||||
const outputPath = path.join(destDir, file)
|
||||
|
||||
if (ext === ".svg" || ext === ".webm") {
|
||||
if (ext === ".svg") {
|
||||
// SVGs just get copied
|
||||
fs.copyFileSync(inputPath, outputPath)
|
||||
console.info(`📸 Copied ${file}`)
|
||||
} else {
|
||||
// Process other images
|
||||
await sharp(inputPath)
|
||||
.resize(1200, 800, {
|
||||
fit: "inside",
|
||||
|
|
|
@ -65,7 +65,7 @@ const Menu = () => {
|
|||
const Header = () => {
|
||||
const { theme, setTheme } = useTheme()
|
||||
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="flex items-center gap-2">
|
||||
<Button variant="ghost" asChild>
|
||||
|
|
|
@ -13,8 +13,8 @@ import { convertDate } from "@/utils/convertDate"
|
|||
const PostListing = ({ posts, title, showAllButton }) => {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-5">
|
||||
<h2 className="scroll-m-20 text-[1.3rem] font-semibold border-b pb-2">
|
||||
<div className="mb-5 ">
|
||||
<h2 className="scroll-m-20 text-2xl font-semibold lg:text-2xl border-b pb-3">
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
|
@ -44,7 +44,7 @@ const PostListing = ({ posts, title, showAllButton }) => {
|
|||
))}
|
||||
</div>
|
||||
{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>
|
||||
</Button>
|
||||
)}
|
||||
|
|
|
@ -2,109 +2,109 @@ import MainTemplate from "@/templates/MainTemplate"
|
|||
import portrait from "../images/portrait-compressed.jpg"
|
||||
|
||||
const AboutPage = () => {
|
||||
return (
|
||||
<MainTemplate>
|
||||
<div className="mb-5 ">
|
||||
<h2 className="scroll-m-20 text-2xl font-semibold lg:text-2xl border-b pb-3">
|
||||
About
|
||||
</h2>
|
||||
</div>
|
||||
return (
|
||||
<MainTemplate>
|
||||
<div className="mb-5 ">
|
||||
<h2 className="scroll-m-20 text-2xl font-semibold lg:text-2xl border-b pb-3">
|
||||
About
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<figure className="w-full flex flex-col items-center mb-6">
|
||||
<img
|
||||
alt="A portrait of the blog author"
|
||||
src={portrait}
|
||||
className="w-0 flex"
|
||||
/>
|
||||
<figcaption className="text-sm text-muted-foreground mt-3 text-center">
|
||||
Pictured with the WITCH computer at the{" "}
|
||||
<a
|
||||
href="https://www.tnmoc.org/"
|
||||
target="_blank"
|
||||
className="text-primary hover:text-primary/80"
|
||||
>
|
||||
National Museum of Computing
|
||||
</a>
|
||||
, Bletchley Park.
|
||||
</figcaption>
|
||||
</figure>
|
||||
<p className="leading-[1.6] [&:not(:first-child)]:mt-6">
|
||||
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
|
||||
technical life so I can have a record of progress when I look back.{" "}
|
||||
</p>
|
||||
<figure className="w-full flex flex-col items-center mb-6">
|
||||
<img
|
||||
alt="A portrait of the blog author"
|
||||
src={portrait}
|
||||
className="w-0 flex"
|
||||
/>
|
||||
<figcaption className="text-sm text-muted-foreground mt-3 text-center">
|
||||
Pictured with the WITCH computer at the{" "}
|
||||
<a
|
||||
href="https://www.tnmoc.org/"
|
||||
target="_blank"
|
||||
className="text-primary hover:text-primary/80"
|
||||
>
|
||||
National Museum of Computing
|
||||
</a>
|
||||
, Bletchley Park.
|
||||
</figcaption>
|
||||
</figure>
|
||||
<p className="leading-[1.5] [&:not(:first-child)]:mt-6">
|
||||
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
|
||||
technical life so I can have a record of progress when I look back.{" "}
|
||||
</p>
|
||||
|
||||
<p className="leading-[1.6] [&:not(:first-child)]:mt-6">
|
||||
I completed a degree in Philosophy at the University of Warwick (2009)
|
||||
and hold a Postgraduate Certificate of Education (2011).
|
||||
</p>
|
||||
<p className="leading-[1.6] [&:not(:first-child)]:mt-6">
|
||||
Currently I work for{" "}
|
||||
<a
|
||||
href="https://en.wikipedia.org/wiki/ITV_(TV_network)"
|
||||
target="_blank"
|
||||
className="underline decoration-1 hover:text-primary/80 underline-offset-2"
|
||||
>
|
||||
ITV
|
||||
</a>{" "}
|
||||
{""}
|
||||
as a backend software engineer. Before that, I worked as a full-stack
|
||||
engineer at the{" "}
|
||||
<a
|
||||
href="https://en.wikipedia.org/wiki/BBC"
|
||||
target="_blank"
|
||||
className="underline decoration-1 hover:text-primary/80 underline-offset-2 font-medium"
|
||||
>
|
||||
BBC
|
||||
</a>{" "}
|
||||
and as a frontend engineer at{" "}
|
||||
<a
|
||||
href="https://www.arria.com/"
|
||||
target="_blank"
|
||||
className="underline decoration-1 hover:text-primary/80 underline-offset-2"
|
||||
>
|
||||
Arria NLG
|
||||
</a>{" "}
|
||||
and in several web developer roles. Before software I was a
|
||||
teacher.{" "}
|
||||
</p>
|
||||
<p className="leading-[1.5] [&:not(:first-child)]:mt-6">
|
||||
I completed a degree in Philosophy at the University of Warwick (2009)
|
||||
and hold a Postgraduate Certificate of Education (2011).
|
||||
</p>
|
||||
<p className="leading-[1.5] [&:not(:first-child)]:mt-6">
|
||||
Currently I work for{" "}
|
||||
<a
|
||||
href="https://en.wikipedia.org/wiki/ITV_(TV_network)"
|
||||
target="_blank"
|
||||
className="underline decoration-1 hover:text-primary/80 underline-offset-2"
|
||||
>
|
||||
ITV
|
||||
</a>{" "}
|
||||
{""}
|
||||
as a backend software engineer. Before that, I worked as a full-stack
|
||||
engineer at the{" "}
|
||||
<a
|
||||
href="https://en.wikipedia.org/wiki/BBC"
|
||||
target="_blank"
|
||||
className="underline decoration-1 hover:text-primary/80 underline-offset-2"
|
||||
>
|
||||
BBC
|
||||
</a>{" "}
|
||||
and as a frontend engineer at{" "}
|
||||
<a
|
||||
href="https://www.arria.com/"
|
||||
target="_blank"
|
||||
className="underline decoration-1 hover:text-primary/80 underline-offset-2"
|
||||
>
|
||||
Arria NLG
|
||||
</a>{" "}
|
||||
and in several web developer roles. Before software I was a
|
||||
teacher.{" "}
|
||||
</p>
|
||||
|
||||
<p className="leading-[1.6] [&:not(:first-child)]:mt-6">
|
||||
Some things I like:
|
||||
<ul className="pt-2">
|
||||
<li className="mb-1">🐶 Staffies and other bull-breeds</li>
|
||||
<li className="mb-1">🎼 Classical music (Haydn, Mozart, JSB)</li>
|
||||
<li className="mb-1">🛸 Science fiction </li>
|
||||
</ul>
|
||||
</p>
|
||||
<p className="leading-[1.5] [&:not(:first-child)]:mt-6">
|
||||
Some things I like:
|
||||
<ul className="pt-2">
|
||||
<li className="mb-1">🐶 Staffies and other bull-breeds</li>
|
||||
<li className="mb-1">🎼 Classical music (Haydn, Mozart, JSB)</li>
|
||||
<li className="mb-1">🛸 Science fiction </li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
<p className="leading-[1.6] [&:not(:first-child)]:mt-6">
|
||||
Some things I'm interested in:
|
||||
<ul className="pt-2">
|
||||
<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">🇮🇪 Irish history and culture</li>
|
||||
<li className="mb-1">☸️ Buddhism</li>
|
||||
{/*
|
||||
<p className="leading-[1.5] [&:not(:first-child)]:mt-6">
|
||||
Some things I'm interested in:
|
||||
<ul className="pt-2">
|
||||
<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">🇮🇪 Irish history and culture</li>
|
||||
<li className="mb-1">☸️ Buddhism</li>
|
||||
{/*
|
||||
|
||||
<li className="mb-1">📡 Civil communications infrastructure</li>
|
||||
*/}
|
||||
</ul>
|
||||
</p>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
<p className="leading-[1.6] [&:not(:first-child)]:mt-6">
|
||||
I self-host my own Git forge at{" "}
|
||||
<a
|
||||
href="https://forgejo.systemsobscure.net/thomasabishop"
|
||||
className="underline decoration-1 hover:text-primary/80 underline-offset-2"
|
||||
>
|
||||
forgejo.systemsobscure.net
|
||||
</a>{" "}
|
||||
rather than use Microsoft GitHub. You can view my personal projects
|
||||
there.
|
||||
</p>
|
||||
</MainTemplate>
|
||||
)
|
||||
<p className="leading-[1.5] [&:not(:first-child)]:mt-6">
|
||||
I self-host my own Git forge at{" "}
|
||||
<a
|
||||
href="https://forgejo.systemsobscure.net/thomasabishop"
|
||||
className="underline decoration-1 hover:text-primary/80 underline-offset-2"
|
||||
>
|
||||
forgejo.systemsobscure.net
|
||||
</a>{" "}
|
||||
rather than use Microsoft GitHub. You can view my personal projects
|
||||
there.
|
||||
</p>
|
||||
</MainTemplate>
|
||||
)
|
||||
}
|
||||
|
||||
export { AboutPage }
|
||||
|
|
|
@ -9,25 +9,14 @@ const HomePage = () => {
|
|||
const { posts } = usePosts()
|
||||
return (
|
||||
<MainTemplate>
|
||||
<div className="mb-7 border border-foreground py-7 px-6 dark:bg-sidebar">
|
||||
<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">
|
||||
<Card className="mb-8 rounded-none">
|
||||
<CardHeader>
|
||||
<h1 className="scroll-m-20 text-left text-2xl font-semibold">
|
||||
Another software engineer with a blog!
|
||||
<h1 className="scroll-m-20 text-left text-3xl font-semibold">
|
||||
Another software engineer with a blog
|
||||
</h1>
|
||||
</CardHeader>
|
||||
<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,
|
||||
previously at the BBC. This blog is a technical scrapbook and
|
||||
digital garden.
|
||||
|
@ -39,13 +28,6 @@ const HomePage = () => {
|
|||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Button asChild>
|
||||
<Link to="/about">About</Link>
|
||||
</Button>
|
||||
|
||||
|
||||
|
||||
*/}
|
||||
|
||||
<PostListing
|
||||
title="Recent posts"
|
||||
|
|
|
@ -4,129 +4,128 @@ import MainTemplate from "@/templates/MainTemplate"
|
|||
import { useParams, useNavigate } from "react-router"
|
||||
import { convertDate } from "@/utils/convertDate"
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from "@/components/ui/pagination"
|
||||
import {
|
||||
Card,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Card,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Link } from "react-router"
|
||||
import { usePosts } from "@/hooks/usePosts"
|
||||
|
||||
const PostsPage = () => {
|
||||
const { page } = useParams()
|
||||
const { page } = useParams()
|
||||
|
||||
const navigate = useNavigate()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const { posts } = usePosts()
|
||||
const postsPerPage = 8
|
||||
const { posts } = usePosts()
|
||||
const postsPerPage = 10
|
||||
|
||||
const currentPage = Number(page) || 1
|
||||
const totalPages = Math.ceil(posts.length / postsPerPage)
|
||||
const currentPage = Number(page) || 1
|
||||
const totalPages = Math.ceil(posts.length / postsPerPage)
|
||||
|
||||
useEffect(() => {
|
||||
// Only redirect if we have posts and the page is definitively invalid
|
||||
if (totalPages > 0 && (currentPage < 1 || currentPage > totalPages)) {
|
||||
navigate(`/posts/page/1`, { replace: true })
|
||||
}
|
||||
}, [currentPage, totalPages, navigate])
|
||||
useEffect(() => {
|
||||
if (currentPage < 1 || currentPage > totalPages) {
|
||||
navigate(`/posts/page/1`, { replace: true })
|
||||
}
|
||||
}, [currentPage, totalPages, navigate])
|
||||
|
||||
const currentPosts = useMemo(() => {
|
||||
const startIndex = (currentPage - 1) * postsPerPage
|
||||
const endIndex = startIndex + postsPerPage
|
||||
return posts.slice(startIndex, endIndex)
|
||||
}, [posts, currentPage, postsPerPage])
|
||||
const currentPosts = useMemo(() => {
|
||||
const startIndex = (currentPage - 1) * postsPerPage
|
||||
const endIndex = startIndex + postsPerPage
|
||||
return posts.slice(startIndex, endIndex)
|
||||
}, [posts, currentPage, postsPerPage])
|
||||
|
||||
const hasNextPage = currentPage < totalPages
|
||||
const hasPrevPage = currentPage > 1
|
||||
const hasNextPage = currentPage < totalPages
|
||||
const hasPrevPage = currentPage > 1
|
||||
|
||||
const goToNextPage = () => {
|
||||
if (hasNextPage) {
|
||||
navigate(`/posts/page/${currentPage + 1}`)
|
||||
}
|
||||
}
|
||||
const goToNextPage = () => {
|
||||
if (hasNextPage) {
|
||||
navigate(`/posts/page/${currentPage + 1}`)
|
||||
}
|
||||
}
|
||||
|
||||
const goToPrevPage = () => {
|
||||
if (hasPrevPage) {
|
||||
navigate(`/posts/page/${currentPage - 1}`)
|
||||
}
|
||||
}
|
||||
const goToPrevPage = () => {
|
||||
if (hasPrevPage) {
|
||||
navigate(`/posts/page/${currentPage - 1}`)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<MainTemplate>
|
||||
<div className="mb-5 ">
|
||||
<h2 className="scroll-m-20 text-2xl font-semibold lg:text-2xl border-b pb-3">
|
||||
All posts
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="grid grid-cols-1 md:grid-cols-1 gap-3 flex-grow">
|
||||
{currentPosts.map((post) => (
|
||||
<Link
|
||||
to={`/posts/${post.slug}`}
|
||||
key={post.slug}
|
||||
className="block no-underline"
|
||||
>
|
||||
<Card
|
||||
key={post.slug}
|
||||
className="flex flex-col h-full hover:bg-primary/5 py-4 px-0 rounded-none"
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle className="leading-snug font-semibold">
|
||||
{post.title}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-sm text-muted-foreground">
|
||||
<div className="flex justify-between gap-2">
|
||||
<span className="text-sm">{convertDate(post.date)}</span>
|
||||
<div className="hidden md:block">
|
||||
{post.tags.map((tag, i) => (
|
||||
<Badge
|
||||
className="ml-2 cursor-pointer"
|
||||
key={i}
|
||||
variant="secondary"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
navigate(`/tags/${tag}`)
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<Pagination className="mt-4">
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
className={`select-none ${hasPrevPage ? "cursor-pointer" : "cursor-not-allowed opacity-50"}`}
|
||||
onClick={goToPrevPage}
|
||||
/>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
className={`select-none ${hasNextPage ? "cursor-pointer" : "cursor-not-allowed opacity-50"}`}
|
||||
onClick={goToNextPage}
|
||||
/>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</div>
|
||||
</MainTemplate>
|
||||
)
|
||||
return (
|
||||
<MainTemplate>
|
||||
<div className="mb-5 ">
|
||||
<h2 className="scroll-m-20 text-2xl font-semibold lg:text-2xl border-b pb-3">
|
||||
All posts
|
||||
</h2>
|
||||
</div>
|
||||
<div className="min-h-[calc(100vh-200px)] flex flex-col">
|
||||
<div className="grid grid-cols-1 md:grid-cols-1 gap-3 flex-grow">
|
||||
{currentPosts.map((post) => (
|
||||
<Link
|
||||
to={`/posts/${post.slug}`}
|
||||
key={post.slug}
|
||||
className="block no-underline"
|
||||
>
|
||||
<Card
|
||||
key={post.slug}
|
||||
className="flex flex-col h-full hover:bg-primary/5 py-4 px-0 rounded-none"
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle className="leading-snug font-semibold">
|
||||
{post.title}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-sm text-muted-foreground">
|
||||
<div className="flex justify-between gap-2">
|
||||
<span className="text-sm">{convertDate(post.date)}</span>
|
||||
<div className="hidden md:block">
|
||||
{post.tags.map((tag, i) => (
|
||||
<Badge
|
||||
className="ml-2 cursor-pointer"
|
||||
key={i}
|
||||
variant="secondary"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
navigate(`/tags/${tag}`)
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<Pagination className="mt-4">
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
className={`select-none ${hasPrevPage ? "cursor-pointer" : "cursor-not-allowed opacity-50"}`}
|
||||
onClick={goToPrevPage}
|
||||
/>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
className={`select-none ${hasNextPage ? "cursor-pointer" : "cursor-not-allowed opacity-50"}`}
|
||||
onClick={goToNextPage}
|
||||
/>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</div>
|
||||
</MainTemplate>
|
||||
)
|
||||
}
|
||||
|
||||
export { PostsPage }
|
||||
|
|
|
@ -37,10 +37,10 @@ const BlogTemplate = () => {
|
|||
</div>
|
||||
<div
|
||||
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
|
||||
[&>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
|
||||
[&>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
|
||||
|
@ -57,10 +57,10 @@ const BlogTemplate = () => {
|
|||
[&>p+pre]:mt-6
|
||||
[&>pre+p]: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
|
||||
[&>p]:mt-6 [&>p]:leading-[1.6]
|
||||
[&_a]:underline [&_a]:underline-offset-4 [&_a]:hover:text-muted-foreground [&_a]:font-medium
|
||||
[&>p]:mt-6 [&>p]:leading-[1.5]
|
||||
[&_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>img]:w-full
|
||||
[&>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