Compare commits

..

18 Commits

Author SHA1 Message Date
Julien Calixte
aeb763dd43 chore: optimize images 2026-05-24 00:54:53 +02:00
Julien Calixte
50d8eb31d6 fix(nginx): redirect unknown paths to homepage
Drop the $uri/ directory step in try_files which triggered the default
403 page on directories without an index, and 302 any 403/404 back to /.
2026-05-23 16:52:12 +02:00
Julien Calixte
9e1fae4f2e feat(pub): add /pub index listing all notes 2026-05-23 16:52:08 +02:00
Julien Calixte
d0175025ad feat(home): show only last 15 notes with link to /pub 2026-05-23 16:52:04 +02:00
Julien Calixte
3153200eb3 refactor(notes): sort fetched notes by publishedAt desc
Centralise ordering in fetchNotes so every consumer (homepage, /pub
index, RSS feed) shares the same newest-first order.
2026-05-23 16:52:01 +02:00
Julien Calixte
ffad0ecf54 docs: describe atproto-backed /pub route 2026-05-23 16:13:50 +02:00
Julien Calixte
71ad6bbc2e docs: rewrite README for apoena.dev 2026-05-23 16:12:56 +02:00
Julien Calixte
aa36e00576 fix: use plc.directory instead of plc.wtf for DID resolution
The plc.wtf mirror is returning HTTP 502, breaking the static build
when getStaticPaths fetches the PDS endpoint. plc.directory is the
canonical PLC directory and returns the same JSON shape.
2026-05-23 16:07:09 +02:00
Julien Calixte
031beba8ac deps: browser 2026-05-03 23:40:22 +02:00
Julien Calixte
044007d495 deps: downgrade for islands 2026-05-03 23:38:15 +02:00
Julien Calixte
dc095af8c4 feat: add a rss feed 2026-05-03 19:13:06 +02:00
Julien Calixte
b8ec3dcff7 fix: copy .npmrc before pnpm install in Dockerfile 2026-03-21 21:49:33 +01:00
Julien Calixte
838082d00b chore: embed nginx config in Dockerfile 2026-03-21 21:48:04 +01:00
Julien Calixte
c42600c570 chore: migrate deployment to Dockerfile
Replace nixpacks.toml with a multi-stage Dockerfile (node:22-alpine builder + nginx:alpine runner) for faster Coolify deployments.
2026-03-21 21:45:40 +01:00
Julien Calixte
2e9487d61f chore: add nixpacks config 2026-03-14 17:22:05 +01:00
Julien Calixte
fef0ac8abf deps: change to md4x for better performance 2026-03-14 13:37:02 +01:00
Julien Calixte
577e10a9d3 feat: add title to note pages 2026-03-14 13:09:59 +01:00
Julien Calixte
975f6dc46b feat: no last notes if there is nothing / todo with skeleton 2026-03-07 11:38:17 +01:00
27 changed files with 2536 additions and 1356 deletions

19
Dockerfile Normal file
View File

@@ -0,0 +1,19 @@
FROM node:22-alpine AS builder
WORKDIR /app
RUN corepack enable && corepack prepare pnpm@latest --activate
COPY package.json pnpm-lock.yaml .npmrc ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -1,50 +1,32 @@
# blog # apoena.dev
[îles]: https://github.com/ElMassimo/iles Julien Calixte's blog, built with [îles](https://github.com/ElMassimo/iles).
[configuration reference]: https://iles-docs.netlify.app/config
This template should help get you started developing with [îles]. Live at [apoena.dev](https://apoena.dev).
## Recommended IDE Setup ## Requirements
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) (and disable Vetur). - Node.js >= 22
- pnpm 10
## Type Support for `.vue` Imports in TS ## Scripts
Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic `PageComponent` type by default. In most cases this is fine if you don't really care about component prop types outside of templates.
However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using manual `h(...)` calls), you can run `Volar: Switch TS Plugin on/off` from VSCode command palette.
## Customize configuration
See îles [Configuration Reference].
## Project Setup
```sh ```sh
npm install pnpm install # install dependencies
pnpm dev # start the dev server
pnpm build # build for production
pnpm preview # preview the production build on port 5050
pnpm typecheck # run vue-tsc
``` ```
### Compile and Hot-Reload for Development ## AT Protocol notes (`/pub`)
```sh Pages under `/pub/:rkey/:slug` are generated at build time from records
npm run dev published to my AT Protocol repository.
```
### Type-Check, Compile and Minify for Production - DID: `did:plc:4m3kouplb7s7xozjd3whinvl`
- Collection: `site.standard.document`
- PDS endpoint is resolved via [plc.directory](https://plc.directory),
then records are listed with `com.atproto.repo.listRecords`.
```sh See `src/api/fetch-notes.ts` and `src/pages/pub/[rkey]/[slug].vue`.
npm run build
```
### Run Unit Tests with [Cypress Component Testing](https://docs.cypress.io/guides/component-testing/introduction)
```sh
npm run test:unit # or `npm run test:unit:ci` for headless testing
```
### Run End-to-End Tests with [Cypress](https://www.cypress.io/)
```sh
npm run test:e2e # or `npm run test:e2e:ci` for headless testing
```

View File

@@ -1,9 +1,9 @@
[build] [build]
publish = "dist" publish = "dist"
command = "npm run build" command = "pnpm run build"
[build.environment] [build.environment]
NODE_VERSION = "16" NODE_VERSION = "22"
[[headers]] [[headers]]
for = "/.well-known/*" for = "/.well-known/*"

23
nginx.conf Normal file
View File

@@ -0,0 +1,23 @@
server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri.html $uri/index.html $uri/index.htm =404;
}
location /.well-known/ {
root /usr/share/nginx/html;
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods "GET, OPTIONS";
}
error_page 403 404 =302 /;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
internal;
}
}

View File

@@ -12,23 +12,27 @@
"node": ">= 22.0.0" "node": ">= 22.0.0"
}, },
"devDependencies": { "devDependencies": {
"@islands/headings": "^0.10.0-beta.1", "@islands/headings": "^0.10.0",
"@islands/prism": "^0.10.0-beta.1", "@islands/prism": "^0.10.0",
"@islands/pwa": "^0.10.0-beta.1", "@islands/pwa": "^0.10.0",
"baseline-browser-mapping": "^2.10.0", "baseline-browser-mapping": "^2.10.27",
"iles": "^0.10.0-beta.1", "iles": "^0.10.0",
"prettier": "^3.8.1", "prettier": "^3.8.3",
"sass": "^1.97.3", "sass": "^1.99.0",
"typescript": "^5.9.3", "typescript": "^6.0.3",
"vitest": "^4.0.18", "vitest": "^4.1.5",
"vue": "^3.5.29", "vue": "^3.5.33",
"vue-tsc": "^3.2.5" "vue-tsc": "^3.2.7",
"vite": "^7.3.2"
}, },
"dependencies": { "dependencies": {
"@atproto/api": "^0.19.0", "@atproto/api": "^0.19.11",
"@islands/feed": "^0.10.0",
"chart.xkcd": "^1.1.15", "chart.xkcd": "^1.1.15",
"marked": "^17.0.3", "marked": "^18.0.3",
"nanoid": "^5.1.6", "md4x": "^0.0.25",
"nanoid": "^5.1.11",
"pinia": "^3.0.4" "pinia": "^3.0.4"
} },
"packageManager": "pnpm@10.33.2"
} }

3666
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -8,7 +8,7 @@ export const getServiceEndpoint = async () => {
if (serviceEndpoint) { if (serviceEndpoint) {
return serviceEndpoint return serviceEndpoint
} }
const response = await fetch(`https://plc.wtf/${did}`) const response = await fetch(`https://plc.directory/${did}`)
const { const {
service: [{ serviceEndpoint: endpoint }], service: [{ serviceEndpoint: endpoint }],
} = await response.json() } = await response.json()
@@ -34,5 +34,8 @@ export const fetchNotes = async (): Promise<Note[]> => {
limit: 50, limit: 50,
}) })
return (records || []).map((record) => record.value) const notes = (records || []).map((record) => record.value as Note)
return notes.sort((a, b) =>
a.publishedAt < b.publishedAt ? 1 : a.publishedAt > b.publishedAt ? -1 : 0,
)
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 451 B

After

Width:  |  Height:  |  Size: 430 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 900 B

After

Width:  |  Height:  |  Size: 879 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -1,16 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import { fetchNotes } from "@/api/fetch-notes" import { fetchNotes } from "@/api/fetch-notes"
const notes = await fetchNotes() const notes = (await fetchNotes()).slice(0, 15)
</script> </script>
<template> <template>
<div class="blog-notes"> <div class="blog-notes" v-if="notes.length > 0">
<h2>Last notes</h2> <h2>Last notes</h2>
<ul> <ul>
<li v-for="note in notes" :key="note.href"> <li v-for="note in notes" :key="note.path">
<a :href="note.path">{{ note.title }}</a> <a :href="note.path">{{ note.title }}</a>
</li> </li>
</ul> </ul>
<p><a href="/pub">See all articles </a></p>
</div> </div>
</template> </template>

34
src/pages/feed.vue Normal file
View File

@@ -0,0 +1,34 @@
<page>
path: /feed.rss
</page>
<script setup lang="ts">
import RenderFeed from "@islands/feed"
import type { FeedOptions, FeedItem } from "@islands/feed"
import { fetchNotes } from "~/api/fetch-notes"
const url = "https://apoena.dev"
const options: FeedOptions = {
title: "Apoena's distracted mind",
description: "Where I write about what I read.",
id: url,
link: url,
language: "en",
image: "https://apoena.dev/pwa-512x512.png",
copyright: "No copyright, just don't forget me",
}
const notes = await fetchNotes()
const items = notes.map<FeedItem>((note) => ({
link: note.canonicalUrl,
date: new Date(note.publishedAt),
title: note.title,
content: note,
}))
</script>
<template>
<RenderFeed format="feed" :options="options" :items="items" />
</template>

View File

@@ -18,11 +18,14 @@ export default definePageComponent({
<script setup lang="ts"> <script setup lang="ts">
import type { Note } from "~/api/note.type" import type { Note } from "~/api/note.type"
import { marked } from "marked" import { renderToHtml } from "md4x"
import { computed } from "vue"
const props = defineProps<{ note: Note }>() const props = defineProps<{ note: Note }>()
const textContent = computed(() => marked.parse(props.note.textContent)) const textContent = renderToHtml(props.note.textContent)
useHead({
title: props.note.title,
})
</script> </script>
<template> <template>

28
src/pages/pub/index.vue Normal file
View File

@@ -0,0 +1,28 @@
<script setup lang="ts">
import { fetchNotes } from "@/api/fetch-notes"
const notes = await fetchNotes()
useHead({
title: "Articles",
})
</script>
<template>
<section class="pub-list">
<h1>Articles</h1>
<ul>
<li v-for="note in notes" :key="note.path">
<a :href="note.path">{{ note.title }}</a>
</li>
</ul>
</section>
</template>
<style scoped lang="scss">
.pub-list {
max-width: 800px;
margin: auto;
padding: 1rem;
}
</style>

View File

@@ -11,7 +11,8 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"esModuleInterop": true, "esModuleInterop": true,
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"],
"~/*": ["./src/*"]
}, },
"lib": ["esnext", "dom", "dom.iterable", "scripthost"], "lib": ["esnext", "dom", "dom.iterable", "scripthost"],
"skipLibCheck": true "skipLibCheck": true