Compare commits
18 Commits
c753026393
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aeb763dd43 | ||
|
|
50d8eb31d6 | ||
|
|
9e1fae4f2e | ||
|
|
d0175025ad | ||
|
|
3153200eb3 | ||
|
|
ffad0ecf54 | ||
|
|
71ad6bbc2e | ||
|
|
aa36e00576 | ||
|
|
031beba8ac | ||
|
|
044007d495 | ||
|
|
dc095af8c4 | ||
|
|
b8ec3dcff7 | ||
|
|
838082d00b | ||
|
|
c42600c570 | ||
|
|
2e9487d61f | ||
|
|
fef0ac8abf | ||
|
|
577e10a9d3 | ||
|
|
975f6dc46b |
19
Dockerfile
Normal 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;"]
|
||||
58
README.md
@@ -1,50 +1,32 @@
|
||||
# blog
|
||||
# apoena.dev
|
||||
|
||||
[îles]: https://github.com/ElMassimo/iles
|
||||
[configuration reference]: https://iles-docs.netlify.app/config
|
||||
Julien Calixte's blog, built with [îles](https://github.com/ElMassimo/iles).
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
## Scripts
|
||||
|
||||
```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
|
||||
npm run dev
|
||||
```
|
||||
Pages under `/pub/:rkey/:slug` are generated at build time from records
|
||||
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
|
||||
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
|
||||
```
|
||||
See `src/api/fetch-notes.ts` and `src/pages/pub/[rkey]/[slug].vue`.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
[build]
|
||||
publish = "dist"
|
||||
command = "npm run build"
|
||||
command = "pnpm run build"
|
||||
|
||||
[build.environment]
|
||||
NODE_VERSION = "16"
|
||||
NODE_VERSION = "22"
|
||||
|
||||
[[headers]]
|
||||
for = "/.well-known/*"
|
||||
|
||||
23
nginx.conf
Normal 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;
|
||||
}
|
||||
}
|
||||
34
package.json
@@ -12,23 +12,27 @@
|
||||
"node": ">= 22.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@islands/headings": "^0.10.0-beta.1",
|
||||
"@islands/prism": "^0.10.0-beta.1",
|
||||
"@islands/pwa": "^0.10.0-beta.1",
|
||||
"baseline-browser-mapping": "^2.10.0",
|
||||
"iles": "^0.10.0-beta.1",
|
||||
"prettier": "^3.8.1",
|
||||
"sass": "^1.97.3",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.0.18",
|
||||
"vue": "^3.5.29",
|
||||
"vue-tsc": "^3.2.5"
|
||||
"@islands/headings": "^0.10.0",
|
||||
"@islands/prism": "^0.10.0",
|
||||
"@islands/pwa": "^0.10.0",
|
||||
"baseline-browser-mapping": "^2.10.27",
|
||||
"iles": "^0.10.0",
|
||||
"prettier": "^3.8.3",
|
||||
"sass": "^1.99.0",
|
||||
"typescript": "^6.0.3",
|
||||
"vitest": "^4.1.5",
|
||||
"vue": "^3.5.33",
|
||||
"vue-tsc": "^3.2.7",
|
||||
"vite": "^7.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.19.0",
|
||||
"@atproto/api": "^0.19.11",
|
||||
"@islands/feed": "^0.10.0",
|
||||
"chart.xkcd": "^1.1.15",
|
||||
"marked": "^17.0.3",
|
||||
"nanoid": "^5.1.6",
|
||||
"marked": "^18.0.3",
|
||||
"md4x": "^0.0.25",
|
||||
"nanoid": "^5.1.11",
|
||||
"pinia": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@10.33.2"
|
||||
}
|
||||
|
||||
3666
pnpm-lock.yaml
generated
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
@@ -8,7 +8,7 @@ export const getServiceEndpoint = async () => {
|
||||
if (serviceEndpoint) {
|
||||
return serviceEndpoint
|
||||
}
|
||||
const response = await fetch(`https://plc.wtf/${did}`)
|
||||
const response = await fetch(`https://plc.directory/${did}`)
|
||||
const {
|
||||
service: [{ serviceEndpoint: endpoint }],
|
||||
} = await response.json()
|
||||
@@ -34,5 +34,8 @@ export const fetchNotes = async (): Promise<Note[]> => {
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 451 B After Width: | Height: | Size: 430 B |
|
Before Width: | Height: | Size: 900 B After Width: | Height: | Size: 879 B |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.0 KiB |
@@ -1,16 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { fetchNotes } from "@/api/fetch-notes"
|
||||
|
||||
const notes = await fetchNotes()
|
||||
const notes = (await fetchNotes()).slice(0, 15)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="blog-notes">
|
||||
<div class="blog-notes" v-if="notes.length > 0">
|
||||
<h2>Last notes</h2>
|
||||
<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>
|
||||
</li>
|
||||
</ul>
|
||||
<p><a href="/pub">See all articles →</a></p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
34
src/pages/feed.vue
Normal 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>
|
||||
@@ -18,11 +18,14 @@ export default definePageComponent({
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Note } from "~/api/note.type"
|
||||
import { marked } from "marked"
|
||||
import { computed } from "vue"
|
||||
import { renderToHtml } from "md4x"
|
||||
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
||||
28
src/pages/pub/index.vue
Normal 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>
|
||||
@@ -11,7 +11,8 @@
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
"@/*": ["./src/*"],
|
||||
"~/*": ["./src/*"]
|
||||
},
|
||||
"lib": ["esnext", "dom", "dom.iterable", "scripthost"],
|
||||
"skipLibCheck": true
|
||||
|
||||