Compare commits

...

75 commits

Author SHA1 Message Date
Ayo
497ce731e2 feat: fix typo in now page 2026-06-18 14:27:33 +02:00
Ayo
42fa96146b 1.3.95 2026-06-18 14:26:01 +02:00
Ayo
d7fa60c9ab 1.3.94 2026-06-18 14:23:11 +02:00
Ayo
52d4fcd5e1 1.3.93 2026-06-18 14:22:26 +02:00
Ayo
37bb719fcd 1.3.92 2026-06-18 14:21:43 +02:00
Ayo
236ccd8001 1.3.91 2026-06-18 14:19:55 +02:00
Ayo
4735626c48 feat: fix typo on now page 2026-06-18 14:19:42 +02:00
Ayo
df959dd8af 1.3.90 2026-06-18 14:17:01 +02:00
Ayo
ffe45fd82e feat: update now label 2026-06-18 14:16:47 +02:00
Ayo
a9cbb5734d 1.3.89 2026-06-18 14:15:03 +02:00
Ayo
181c951965 feat: new now page content 2026-06-18 14:14:27 +02:00
Ayo
0876ee112c chore: remove post commit push tags 2026-06-18 13:27:02 +02:00
Ayo
1bb0b32304 style: make now label border solid 2026-06-18 13:25:51 +02:00
Ayo
962f2d5dd6 chore: also mirror tags 2026-06-16 08:02:16 +02:00
Ayo
d8a38b60e3 1.3.88 2026-06-10 12:00:34 +02:00
Ayo
1ae9325ace feat: link to blog prod 2026-06-10 11:46:22 +02:00
Ayo
73ecdbe4d8 1.3.87 2026-06-08 11:15:40 +02:00
Ayo
bd65162c70 style: dotted bg for whole page 2026-06-08 11:15:21 +02:00
Ayo
fb93eb7574 1.3.86 2026-06-07 22:26:02 +02:00
Ayo
ed4b091cf1 refactor: remove unneeded style 2026-06-07 22:25:54 +02:00
Ayo
778328d984 1.3.85 2026-06-07 22:25:30 +02:00
Ayo
44e6fe6722 style: dotted bg everywhere 2026-06-07 22:25:03 +02:00
Ayo
271a92c7b3 1.3.84 2026-06-07 22:21:55 +02:00
Ayo
d79e51f1ce style: dotted background 2026-06-07 22:21:38 +02:00
Ayo
cdc9d091e3 1.3.83 2026-06-05 19:27:59 +02:00
Ayo
ef5c186345 feat: use elk.zone 2026-06-05 19:27:43 +02:00
Ayo
8c2e355bfb 1.3.82 2026-06-05 17:16:55 +02:00
Ayo
a9dc107176 feat: status-indicator on now-label 2026-06-05 17:16:29 +02:00
Ayo
ec7442da30 1.3.81 2026-06-05 12:24:39 +02:00
Ayo
7a674694a7 1.3.80 2026-06-05 12:16:50 +02:00
Ayo
d8768998e2 chore(deps): add sharp 2026-06-05 12:16:28 +02:00
Ayo
2f39f3d62d 1.3.79 2026-06-05 12:14:36 +02:00
Ayo
28a57aca9a chore: update packageManager 2026-06-05 12:14:22 +02:00
Ayo
e77351e5dd refactor: trying copilot edits 2026-06-05 12:13:13 +02:00
Ayo
fd1d015a18 chore(pgp): format 2026-06-05 12:13:13 +02:00
Ayo
f2bbfbf1b4 chore: add test config and instructions 2026-06-05 12:13:13 +02:00
Ayo
4860085aef chore: update lint and format scripts 2026-06-05 12:13:13 +02:00
Ayo
e3be0e92cb feat: update mastodon to webtoo.ls profile 2026-06-05 12:12:29 +02:00
Ayo
1dbd3d662c 1.3.78 2026-05-31 18:48:18 +02:00
Ayo
199f3d1429 fix: links to legacy blog 2026-05-31 18:47:57 +02:00
Ayo
74a3a71090 1.3.77 2026-05-31 18:41:01 +02:00
Ayo
7966746711 feat(pgp): copy & download buttons 2026-05-31 18:40:50 +02:00
Ayo
7e0380a324 1.3.76 2026-05-31 17:04:22 +02:00
Ayo
7921721425 feat(pgp): concise description 2026-05-31 17:04:11 +02:00
Ayo
5fa6e68b37 1.3.75 2026-05-31 17:00:38 +02:00
Ayo
a71eb0f975 feat: add 'encryption' purpose for pgp key 2026-05-31 16:28:29 +02:00
Ayo
087403f70f 1.3.74 2026-05-31 13:11:35 +02:00
Ayo
25a83048d5 feat: remove chat link 2026-05-31 13:11:09 +02:00
Ayo
8f09af3829 1.3.73 2026-05-31 10:08:15 +02:00
Ayo
7ff435d619 feat(pgp): concise desc 2026-05-31 10:07:15 +02:00
Ayo
9d21f0282b 1.3.72 2026-05-31 08:42:01 +02:00
Ayo
5cc77e537b fix(pgp): meta descriptions 2026-05-31 08:41:42 +02:00
Ayo
05dd11db99 chore: patch, build, deploy script shortcut 2026-05-31 08:37:36 +02:00
Ayo
2e4cf74810 1.3.71 2026-05-31 08:34:50 +02:00
Ayo
5140c1fe55 feat(pgp): add title and description 2026-05-31 08:34:37 +02:00
Ayo
d9abb4bd53 1.3.70 2026-05-31 02:46:28 +02:00
Ayo
5104dc3176 feat: indicate pgp key expiry date 2026-05-31 02:44:33 +02:00
Ayo
5d2802aca7 feat: copy-able public key 2026-05-31 02:40:12 +02:00
Ayo
f0bb8f9272 feat: pgp key download link 2026-05-31 02:21:03 +02:00
Ayo
e4081c42e4 1.3.69 2026-05-30 23:04:13 +02:00
Ayo
9e4eb6ef15 feat: add PGP key 2026-05-30 23:03:59 +02:00
Ayo
a6f7c1153c feat: add gpg publickey 2026-05-30 22:05:13 +02:00
Ayo
cea29e7a01 chore: use eslint config helpers 2026-05-28 22:12:53 +02:00
Ayo
eaecdbcf23 chore: add eslint & prettier to workspace extensions 2026-05-28 22:09:52 +02:00
Ayo
9d69a6cc67 chore: add vitest-dom & sw test 2026-05-24 15:15:51 +02:00
Ayo
9b4312b49d test: prep-now 2026-05-23 23:26:32 +02:00
Ayo
bb335badf6 chore: add vitest 2026-05-23 23:25:44 +02:00
Ayo
85f9b148de chore: set allowBuild to false 2026-05-23 23:17:28 +02:00
Ayo
ca81b780ce chore: update pnpm & add allowBuild config 2026-05-23 23:16:48 +02:00
Ayo
0f28734a2d chore: add patch build deploy command to readme 2026-05-20 18:08:15 +02:00
Ayo
df5b89e5b1 chore: format 2026-05-17 11:20:17 +02:00
Ayo
8861d7f73d 1.3.68 2026-05-16 16:09:59 +02:00
Ayo
42112a8a9f feat: responsive now-wrapper 2026-05-16 16:09:18 +02:00
Ayo
08ef1cd749 1.3.67 2026-05-15 20:34:03 +02:00
Ayo
79c31a77d3 chore(deps): update astro-sw to v1 2026-05-15 20:33:18 +02:00
23 changed files with 1199 additions and 384 deletions

View file

@ -1,4 +1,8 @@
{
"recommendations": ["astro-build.astro-vscode"],
"recommendations": [
"astro-build.astro-vscode",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
],
"unwantedRecommendations": []
}

View file

@ -22,12 +22,30 @@ $ pnpm i
## Commands
| Command | Action |
| ------------------- | ---------------------------------------------- |
| `pnpm run dev` | start dev server |
| `pnpm run build` | generate static files to `dist` directory |
| `pnpm run deploy` | upload to my server |
| `pnpm run prep:now` | back up and clear current `now page` constants |
| Command | Action |
| ----------------------------- | ---------------------------------------------- |
| `pnpm run dev` | start dev server |
| `pnpm run build` | generate static files to `dist` directory |
| `pnpm run deploy` | upload to my server |
| `pnpm run prep:now` | back up and clear current `now page` constants |
| `pnpm run patch:build:deploy` | increment version, build, then upload |
## Testing
This project uses Vitest for unit tests. The test script is defined in `package.json` as `vitest run .`.
Run tests locally with pnpm (recommended):
```bash
# install dependencies (if you haven't already)
pnpm i
# run tests once
pnpm test
# run Vitest in interactive/watch mode
pnpm exec vitest
```
## Deployment

View file

@ -5,7 +5,7 @@ import tseslint from 'typescript-eslint'
import astroSwGlobals from '@ayco/astro-sw/globals'
import astroParser from 'astro-eslint-parser'
import { includeIgnoreFile } from '@eslint/compat'
import { includeIgnoreFile } from '@eslint/config-helpers'
import path from 'node:path'
import { fileURLToPath } from 'node:url'

View file

@ -1,7 +1,7 @@
{
"name": "@ayco/personal-website",
"type": "module",
"version": "1.3.66",
"version": "1.3.95",
"private": true,
"scripts": {
"astro": "astro",
@ -11,59 +11,66 @@
"preview": "astro preview",
"lint": "eslint . --config eslint.config.mjs --cache",
"lint:fix": "eslint . --config eslint.config.mjs --fix",
"format": "prettier . --write",
"check": "npm run format && npm run lint",
"fmt": "prettier . --config prettier.config.mjs --check",
"fmt:fix": "prettier . --config prettier.config.mjs --write",
"check": "npm run fmt && npm run lint",
"deploy": "eval $(grep '^HOST' .env) && scp -r dist ayo@$HOST:~/ayco.io-flask",
"patch:build:deploy": "npm version patch && npm run build && npm run deploy",
"pbd": "npm run patch:build:deploy",
"build:preview": "npm run build && astro preview",
"build:deploy": "npm run build && npm run deploy",
"copy:dist": "npm run build && cp -R dist ../ayco.io-flask/",
"prepare": "husky",
"prep:now": "npx jiti ./commands/prep-now.js"
"prep:now": "npx jiti ./commands/prep-now.js",
"test": "vitest run ."
},
"devDependencies": {
"@astrojs/sitemap": "^3.7.2",
"@ayco/astro-sw": "^0.10.1",
"@ayco/astro-sw": "^1.0.0",
"@eslint/compat": "^2.1.0",
"@eslint/config-helpers": "^0.6.0",
"@eslint/js": "^10.0.1",
"@iconify-json/mdi": "^1.2.3",
"@iconify-json/simple-icons": "^1.2.81",
"@iconify-json/tabler": "^1.2.34",
"@typescript-eslint/parser": "^8.59.2",
"astro": "^6.3.1",
"@iconify-json/simple-icons": "^1.2.82",
"@iconify-json/tabler": "^1.2.35",
"@typescript-eslint/parser": "^8.59.3",
"astro": "^6.3.3",
"astro-eslint-parser": "^1.4.0",
"astro-github-stats": "^0.8.0",
"astro-icon": "^1.1.5",
"consola": "^3.4.2",
"eslint": "^10.3.0",
"eslint": "^10.4.0",
"eslint-plugin-astro": "^1.7.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"globals": "^17.6.0",
"husky": "^9.1.7",
"jiti": "^2.7.0",
"lint-staged": "^17.0.2",
"lint-staged": "^17.0.4",
"pathe": "^2.0.3",
"prettier": "^3.8.3",
"prettier-plugin-astro": "^0.14.1",
"rehype-stringify": "^10.0.1",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.2",
"sharp": "^0.34.5",
"tslib": "^2.8.1",
"typescript": "^6.0.3",
"typescript-eslint": "^8.59.2",
"typescript-eslint": "^8.59.3",
"unified": "^11.0.5",
"vite-plugin-static-copy": "^4.1.0"
"vite-plugin-static-copy": "^4.1.0",
"vitest": "^4.1.7",
"vitest-dom": "^0.1.1"
},
"lint-staged": {
"*.{js,mjs,astro,ts}": [
"prettier --write",
"eslint --fix"
"prettier . --config prettier.config.mjs --write",
"eslint . --config eslint.config.mjs --fix"
],
"*.json": [
"prettier --write"
"prettier . --config prettier.config.mjs --write"
]
},
"packageManager": "pnpm@10.33.2",
"packageManager": "pnpm@11.5.2+sha512.71c631e382066efc25625d5cf029075de07b61b37f6e27350fbd84b1bda5864c8c1967adc280776b45c30a715c0359a3be08fef42d5bb09e2b99029979692916",
"dependencies": {
"@ayo-run/status-indicator": "^2.1.2",
"web-component-base": "^4.1.2"

File diff suppressed because it is too large Load diff

4
pnpm-workspace.yaml Normal file
View file

@ -0,0 +1,4 @@
allowBuilds:
esbuild: false
sharp: false
web-component-base: false

13
public/publickey.asc Normal file
View file

@ -0,0 +1,13 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEahs0NhYJKwYBBAHaRw8BAQdAV2CfblniKxklPgW9eYt2gBl0jMHLwtjrQaY+
BQoWdIa0FkF5byBBeWNvIDxheW9AYXljby5pbz6ImQQTFgoAQRYhBBfxPV6P9zcr
E1RcOGXmv2QVKTxlBQJqGzQ2AhsDBQkB4TOABQsJCAcCAiICBhUKCQgLAgQWAgMB
Ah4HAheAAAoJEGXmv2QVKTxlIeYA/2WLvkDapBbVmGXoACAhqcTN93/CoPyzUbhN
hHE6mmAUAQDHsgCHmh3S/Rn4NRE3Gb41kiPTBWHrlYLTj3Kiw0ASB7g4BGobNDYS
CisGAQQBl1UBBQEBB0C6OEKEIPLaNDOM6JYhvvq0Q4Mk/B1eyZBKF/b4fQvlGgMB
CAeIfgQYFgoAJhYhBBfxPV6P9zcrE1RcOGXmv2QVKTxlBQJqGzQ2AhsMBQkB4TOA
AAoJEGXmv2QVKTxlMSQBAP5ta1kUFp3HAYwcun8qmoiVq1dEJSN1LnI7HlX4ucTl
AP0YLC768PFTBm9CM5T1BE0xjJ7s4dZSrVoI4n8RSe1nCA==
=MTsv
-----END PGP PUBLIC KEY BLOCK-----

View file

@ -0,0 +1,7 @@
{
"title": "Social Web & Web Components",
"description": "Getting into discussions about improving the social web & building components",
"publishDate": "2026-05-12",
"publishedOn": "",
"publishState": ""
}

View file

@ -0,0 +1,13 @@
I bought a pull-up bar, and in addition to a few barbells at home, I am starting to have a mini gym.
Super happy that I am getting healthier as I build my workout routines.
To keep my mind sharp, I find it helpful to have several hobby things I can get back to from time to time.
I am in a few discussions for exploring some things in the open social web. Details can come later as they become more concrete.
Soft launch of my own newsletter is up on the canary version of my blog: https://main.ayos.blog/subscribe -- because email is the first decentralized social platform. Let's explore that idea more in the coming months.
I'm also back on my webcomponents main arc, after some side quests in self-hosting & machine learning.
Links to come later. Cheers!

View file

@ -57,7 +57,7 @@ export const footerLinks: Link[] = [
},
{
text: 'Mastodon',
url: 'https://main.elk.zone/social.ayco.io/@ayo',
url: 'https://elk.zone/m.webtoo.ls/@ayo',
icon: 'mastodon',
},
{

View file

@ -1,7 +1,7 @@
{
"title": "Social Web & Web Components",
"description": "Getting into discussions about improving the social web & building components",
"publishDate": "2026-05-12",
"publishedOn": "",
"publishState": ""
"title": "Fam++, Crypto, ~AI, OSS",
"description": "Navigating changes in the family and the tech industry",
"publishDate": "2026-06-18",
"publishedOn": "the 18th of June 2026",
"publishState": "in Amsterdam"
}

View file

@ -1,13 +1,21 @@
I bought a pull-up bar, and in addition to a few barbells at home, I am starting to have a mini gym.
## 2 vs 2 in the house
Super happy that I am getting healthier as I build my workout routines.
A new challenger is entering the game. It is safe to share now that we're expecting a new addition to the family: a baby girl is joining us later this year. I have been enjoying having a mini-me in the form of my lil kiddo, Kahel, and now it's time for Jen to have hers.
To keep my mind sharp, I find it helpful to have several hobby things I can get back to from time to time.
## Cryptography
I am in a few discussions for exploring some things in the open social web. Details can come later as they become more concrete.
The latest rabbit hole for my tech adventures is on [cryptography](https://elk.zone/m.webtoo.ls/@ayo@ayco.io/116667802537651710); particularly OpenPGP stuff and the implementation on signing & verifying emails. Check [my public key](/pgp) if you want to exchange encrypted messages :)
Soft launch of my own newsletter is up on the canary version of my blog: https://main.ayos.blog/subscribe -- because email is the first decentralized social platform. Let's explore that idea more in the coming months.
Technically this key-pair cryptography can be applied to any data exchange, so I naturally have been ruminating on how we can bring this to the decentralized social web.
I'm also back on my webcomponents main arc, after some side quests in self-hosting & machine learning.
## "AI"
Links to come later. Cheers!
I disassembled the eGPU setup I have for experimenting with local LLM inference and built a new PC. It is now a dedicated 24/7 "AI" computer at home that powers my [Open WebUI](https://ai.ayo.run) service. Additionally it runs [hermes](https://hermes-agent.nousresearch.com/) which is a "totally safe private local agentic AI" architecture -- and I got to say, it is the most successful one I've tried yet with local AI stuff.
Where am I using this? I don't know yet. It's purely for understanding the "agentic" stuff
## Open source
- [Elk](https://elk.zone) - I try to help out reviewing PRs and will pick up coding work again
- [Webtoo.ls](https://webtoo.ls) - I picked up administration work for this and have BIG dreams on bringing more Web ecosystem open source presence to the Fediverse
- [others](https://git.ayo.run/ayo) - A lot of my other explorations are out in the open

View file

@ -15,7 +15,7 @@ import { getImage } from 'astro:assets'
// fetch mastodon account
const response = await fetch(
'https://social.ayco.io/api/v1/accounts/lookup?acct=ayo'
'https://m.webtoo.ls/api/v1/accounts/lookup?acct=ayo'
)
const data = await response.json()
const { avatar } = data
@ -51,13 +51,28 @@ const ogFileType = 'image/png'
font-size: var(--font-size-base);
color: var(--text-color-dark);
background-color: var(--text-color-light);
background-image: radial-gradient(
circle,
hsl(var(--dot-grid-light)) 1px,
transparent 1px
);
background-size: 24px 24px;
background-position: -24px -24px;
}
@media (prefers-color-scheme: dark) {
html,
body,
* {
background: var(--bg-darker);
background-color: var(--bg-darker);
color: var(--text-color-light);
background-image: radial-gradient(
circle,
hsl(var(--dot-grid-dark)) 1px,
transparent 1px
);
}
}

View file

@ -27,7 +27,6 @@
--bg-darker: #212529;
--bg-darkest: #000;
--pill-border-color: rgba(255, 255, 255, 0.2);
--pill-border-radius: 40px;
--pill-padding: 8px 4px;
--dot-grid-light: 214 32% 82%;
--dot-grid-dark: 215 25% 25%;
}

View file

@ -52,11 +52,9 @@ import Footer from '../components/Footer.astro'
<h2 id="contact">Contact info</h2>
<p>My inbox is open to everyone.</p>
<ul>
<li>✉️ Email me: <a href="mailto:hi@ayo.run">hi@ayo.run</a></li>
<li>
💬 Let's chat: <a href="https://chat.ayo.run/join/7IKG-h3nW-pD1H"
>chat.ayo.run</a
>
✉️ Email me: <a href="mailto:ayo@ayco.io">ayo@ayco.io</a> &middot
<small> (<a href="/pgp">PGP key</a>)</small>
</li>
<li>
💬 Signal: <a
@ -68,11 +66,11 @@ import Footer from '../components/Footer.astro'
</ul>
</main>
<Footer />
</Layout>
<style>
ul {
list-style: none;
padding-left: 0;
}
</style>
<style>
ul {
list-style: none;
padding-left: 0;
}
</style>
</Layout>

View file

@ -8,7 +8,7 @@ import { Picture } from 'astro:assets'
// fetch mastodon account
const response = await fetch(
'https://social.ayco.io/api/v1/accounts/lookup?acct=ayo'
'https://m.webtoo.ls/api/v1/accounts/lookup?acct=ayo'
)
const data = await response.json()
const { avatar, note } = data
@ -36,33 +36,22 @@ const avatarSize = 150
<h1 title="Ayo Ayco | Software Engineer + Web Developer">
Hi, I'm <span class="heavy-text">Ayo</span>!
</h1>
<!--a href="https://forms.ayo.run/form/tnz7FybY" class="now-wrapper"-->
<a href="/now" class="now-wrapper pill">
<span class="now-label">now</span>
<a href="/now" class="now-wrapper action primary">
<span class="now-label">
<status-indicator status="active" pulse> now </status-indicator>
</span>
<span class="status">{now.title}</span>
</a>
<a
href="https://chat.ayo.run/join/7IKG-h3nW-pD1H"
class="chat-link pill"
>
<status-indicator id="chat-link" pulse status="positive">
Chat
</status-indicator>
</a>
</div>
</div>
</section>
<main>
<main class=".dot-grid">
<section class="introduction-section">
<p>
I care about the <em>Web</em>, and I love to <em>create</em> stuff to <em
>inspire</em
> and <em>serve</em> others.
<!--
<a href="/about">More?</a>
-->
</p>
</section>
<section class="cards-section">
@ -96,13 +85,30 @@ const avatarSize = 150
color: white;
}
.pill {
.action {
margin-right: 0.5rem;
padding: var(--pill-padding);
padding: 8px 4px;
color: white;
font-size: var(--font-size-sm);
text-decoration: none;
font-weight: normal;
&.primary {
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 40px;
}
}
.now-wrapper {
display: inline-block;
max-width: 390px;
white-space: nowrap;
overflow: clip;
text-overflow: ellipsis;
.status {
max-width: 300px;
}
}
.now-label {
@ -202,7 +208,7 @@ const avatarSize = 150
}
@media only screen and (max-device-width: 360px) {
.pill {
.action.primary {
border: 0px;
border-radius: 0;
font-size: var(--font-size-sm);
@ -233,6 +239,12 @@ const avatarSize = 150
}
@media only screen and (min-device-width: 280px) and (max-device-width: 653px) {
.action {
display: block;
}
.now-wrapper {
max-width: 600px;
}
.highlighted-section__content {
padding: 1rem;
}

View file

@ -37,7 +37,6 @@ publishedOn = publishedOn === '' ? publishDate : publishedOn
</p>
<Fragment set:html={content} />
</main>
<Footer />
</Layout>

View file

@ -0,0 +1,72 @@
---
import Layout from '../../../../layouts/Layout.astro'
import Footer from '../../../../components/Footer.astro'
const title = `Social Web & Web Components`
const description = `Getting into discussions about improving the social web & building components`
let publishedOn = ``
const publishDate = `2026-05-12`
const publishState = ``
const content = `<p>I bought a pull-up bar, and in addition to a few barbells at home, I am starting to have a mini gym.</p>
<p>Super happy that I am getting healthier as I build my workout routines.</p>
<p>To keep my mind sharp, I find it helpful to have several hobby things I can get back to from time to time.</p>
<p>I am in a few discussions for exploring some things in the open social web. Details can come later as they become more concrete.</p>
<p>Soft launch of my own newsletter is up on the canary version of my blog: https://main.ayos.blog/subscribe -- because email is the first decentralized social platform. Let's explore that idea more in the coming months.</p>
<p>I'm also back on my webcomponents main arc, after some side quests in self-hosting &#x26; machine learning.</p>
<p>Links to come later. Cheers!</p>`
publishedOn = publishedOn === '' ? publishDate : publishedOn
---
<Layout title={title} description={description}>
<main>
<h1><span class="text-gradient">{title}</span></h1>
<p>
<small>
Published on
<time datetime={publishDate}>
{publishedOn}
</time>
{publishState}
</small>
</p>
<Fragment set:html={content} />
</main>
<Footer />
</Layout>
<style>
.text-gradient {
font-weight: 900;
background-image: var(--ayo-gradient);
animation: pulse 4s ease-in-out infinite;
background-size: 500% 500%;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-size: 100% 200%;
background-position-y: 100%;
border-radius: 0.4rem;
}
.highlighted-content {
margin: 1rem 0;
background: #4f39fa;
padding: 1rem;
border-radius: 0.4rem;
color: var(--color-bg);
}
.highlighted-content code {
font-size: var(--font-size-base);
border: 0.1em solid var(--color-border);
border-radius: 4px;
padding: 0.15em 0.25em;
}
.link-card-grid {
display: grid;
gap: 1rem;
padding: 0;
}
</style>

132
src/pages/pgp.astro Normal file
View file

@ -0,0 +1,132 @@
---
import Layout from '../layouts/Layout.astro'
import Footer from '../components/Footer.astro'
const title = 'PGP public key'
const description =
'Use to verify my digital signature or to encrypt messages intended only for me.'
---
<Layout title={"Ayo's " + title} description={description}>
<main>
<h1>My {title}</h1>
<p>{description}</p>
<div class="key-block" role="region" aria-labelledby="public-key">
<div class="btn-wrapper">
<button class="copy-btn btn" aria-label="Copy key to clipboard"
>Copy</button
>
<a href="/publickey.asc" class="btn">Download</a>
</div>
<pre
id="public-key"><code>
-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEahs0NhYJKwYBBAHaRw8BAQdAV2CfblniKxklPgW9eYt2gBl0jMHLwtjrQaY+
BQoWdIa0FkF5byBBeWNvIDxheW9AYXljby5pbz6ImQQTFgoAQRYhBBfxPV6P9zcr
E1RcOGXmv2QVKTxlBQJqGzQ2AhsDBQkB4TOABQsJCAcCAiICBhUKCQgLAgQWAgMB
Ah4HAheAAAoJEGXmv2QVKTxlIeYA/2WLvkDapBbVmGXoACAhqcTN93/CoPyzUbhN
hHE6mmAUAQDHsgCHmh3S/Rn4NRE3Gb41kiPTBWHrlYLTj3Kiw0ASB7g4BGobNDYS
CisGAQQBl1UBBQEBB0C6OEKEIPLaNDOM6JYhvvq0Q4Mk/B1eyZBKF/b4fQvlGgMB
CAeIfgQYFgoAJhYhBBfxPV6P9zcrE1RcOGXmv2QVKTxlBQJqGzQ2AhsMBQkB4TOA
AAoJEGXmv2QVKTxlMSQBAP5ta1kUFp3HAYwcun8qmoiVq1dEJSN1LnI7HlX4ucTl
AP0YLC768PFTBm9CM5T1BE0xjJ7s4dZSrVoI4n8RSe1nCA==
=MTsv
-----END PGP PUBLIC KEY BLOCK-----
</code></pre>
</div>
<p>Expiry Date: 2027-05-30</p>
<p>
Fingerprint: <code>17F1 3D5E 8FF7 372B 1354 5C38 65E6 BF64 1529 3C65</code
>
</p>
<p></p>
</main>
<Footer />
</Layout>
<script>
/* Clipboard copy logic */
const copyBtn = document.querySelector('.copy-btn')
if (copyBtn) {
copyBtn.addEventListener('click', async () => {
const keyText = (
document.querySelector('#public-key code') as HTMLElement
)?.innerText.trim()
if (!keyText) return
try {
await navigator.clipboard.writeText(keyText)
const btn = document.querySelector('.copy-btn')
if (btn) {
const original = btn.textContent
btn.textContent = 'Copied!'
setTimeout(() => {
btn.textContent = original
}, 2000)
}
} catch (err) {
console.error('Copy failed', err)
alert('Unable to copy the key. Please copy it manually.')
}
})
}
</script>
<style>
code {
font-size: var(--font-size-sm);
}
.key-block {
position: relative;
background: #272822;
color: #f8f8f2;
font-family: monospace;
font-size: 0.9rem;
line-height: 1.4;
padding: 1rem 1rem 1rem 1.5rem;
border-radius: 4px;
overflow-x: auto;
white-space: pre-wrap;
word-wrap: break-word;
margin: 1em 0;
}
.key-block code {
display: block;
}
/* buttons */
.btn-wrapper {
position: absolute;
top: 0.5rem;
right: 0.5rem;
}
.btn {
display: inline-block;
position: relative;
background: var(--color-brand-blue-1);
color: #fff;
border: none;
border-radius: 4px;
padding: 0.3rem 0.6rem;
font-size: 0.8rem;
cursor: pointer;
opacity: 0.9;
transition: opacity 0.2s;
text-decoration: none;
}
.btn:hover,
.btn:focus {
opacity: 1;
}
.btn:focus {
outline: 2px solid #0056b3;
outline-offset: 2px;
}
</style>

View file

@ -11,7 +11,7 @@ import Card from '../components/Card.astro'
<main>
<h1><span class="text-gradient">Hobby Projects</span></h1>
<p>
See more of my previous projects at <a href="https://ayos.blog"
See more of my previous projects at <a href="https://ayos.blog/projects"
>my blog</a
>.
</p>

121
tests/prep-now.test.ts Normal file
View file

@ -0,0 +1,121 @@
import { readFileSync, writeFileSync, copyFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, resolve } from 'node:path'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { consola } from 'consola'
import newNow from '../commands/prep-now'
// Mock file system operations
vi.mock('path', () => ({
resolve: vi.fn(),
readFileSync: vi.fn(),
writeFileSync: vi.fn(),
copyFileSync: vi.fn(),
}))
// Mock consola
vi.mock('consola', () => ({
consola: {
box: vi.fn(),
start: vi.fn(),
success: vi.fn(),
fail: vi.fn(),
error: vi.fn(),
},
}))
// Mock mdToHTML
vi.mock('../command/md-to-html', () => ({
mdToHTML: vi.fn(),
}))
describe('prep-now', () => {
// const mockNow = {
// title: 'Test Title',
// description: 'Test Description',
// publishedOn: '2023-01-01',
// publishDate: '2023-01-01',
// publishState: 'draft',
// content: '# Test Content',
// }
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(resolve).mockImplementation((...paths) => paths.join('/'))
vi.mocked(readFileSync).mockReturnValue('template content')
vi.mocked(writeFileSync).mockImplementation(() => {})
vi.mocked(copyFileSync).mockImplementation(() => {})
vi.mocked(consola.box).mockImplementation(() => {})
vi.mocked(consola.start).mockImplementation(() => {})
vi.mocked(consola.success).mockImplementation(() => {})
vi.mocked(consola.fail).mockImplementation(() => {})
vi.mocked(consola.error).mockImplementation(() => {})
})
it('should create a new now page and clear the original files', async () => {
// Mock the mdToHTML function
const { mdToHTML } = await import('../utils/md-to-html')
vi.mocked(mdToHTML).mockResolvedValue('<h1>Test Content</h1>')
// Mock file paths
vi.mocked(resolve)
.mockReturnValueOnce('/src/constants/now.md')
.mockReturnValueOnce('/src/constants/now.json')
.mockReturnValueOnce('/src/pages/now/and-then/posts/2023-01-01.astro')
.mockReturnValueOnce('/src/constants/bkup/2023-01-01.md')
.mockReturnValueOnce('/src/constants/bkup/2023-01-01.json')
// Mock file content
vi.mocked(readFileSync)
.mockReturnValueOnce('template content')
.mockReturnValueOnce('# Test Content')
// Mock file paths
const __filename = '/src/commands/prep-now.ts'
const __dirname = '/src/commands'
vi.mocked(fileURLToPath).mockReturnValue(__filename)
vi.mocked(dirname).mockReturnValue(__dirname)
// Execute the function
await newNow()
// Verify file operations
expect(writeFileSync).toHaveBeenCalledTimes(2)
expect(copyFileSync).toHaveBeenCalledTimes(2)
expect(consola.success).toHaveBeenCalledWith('now.md cleared')
expect(consola.success).toHaveBeenCalledWith(
'You may now update your Now content and props.\n'
)
})
it('should handle errors gracefully', async () => {
// Mock an error
vi.mocked(writeFileSync).mockImplementation(() => {
throw new Error('Write failed')
})
// Mock file paths
vi.mocked(resolve)
.mockReturnValueOnce('/src/constants/now.md')
.mockReturnValueOnce('/src/constants/now.json')
.mockReturnValueOnce('/src/pages/now/and-then/posts/2023-01-01.astro')
.mockReturnValueOnce('/src/constants/bkup/2023-01-01.md')
.mockReturnValueOnce('/src/constants/bkup/2023-01-01.json')
// Mock file content
vi.mocked(readFileSync)
.mockReturnValueOnce('template content')
.mockReturnValueOnce('# Test Content')
// Execute the function
await newNow()
// Verify error handling
expect(consola.fail).toHaveBeenCalledWith(
'Failed to create a new post from Now'
)
expect(consola.error).toHaveBeenCalled()
})
})

91
tests/sw.test.js Normal file
View file

@ -0,0 +1,91 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { install } from 'vitest-dom'
// Install DOM environment for vitest
install()
// Mock the service worker globals
global.caches = {
keys: vi.fn(),
delete: vi.fn(),
open: vi.fn(),
match: vi.fn(),
}
// Mock the global self object
global.self = {
skipWaiting: vi.fn(),
addEventListener: vi.fn(),
}
// Mock the fetch function
global.fetch = vi.fn()
// Import the service worker module
// Note: We need to use dynamic import to avoid module loading issues
let swModule
beforeEach(async () => {
// Clear all mocks
vi.clearAllMocks()
// Mock the module
swModule = await import('../src/sw.mjs')
})
describe('Service Worker', () => {
describe('cleanOldCaches', () => {
it('should delete old caches', async () => {
const cacheName = 'app-v000'
const oldCacheName = 'app-v001'
global.caches.keys.mockResolvedValue([cacheName, oldCacheName])
global.caches.delete.mockResolvedValue(true)
await swModule.cleanOldCaches()
expect(global.caches.delete).toHaveBeenCalledWith(oldCacheName)
})
})
describe('addResourcesToCache', () => {
it('should add resources to cache', async () => {
const resources = ['/index.html', '/style.css']
const cache = { addAll: vi.fn() }
global.caches.open.mockResolvedValue(cache)
await swModule.addResourcesToCache(resources)
expect(cache.addAll).toHaveBeenCalledWith(resources)
})
})
describe('networkFirst', () => {
it('should return network response when available', async () => {
const request = new Request('/test')
const networkResponse = new Response('network content')
global.fetch.mockResolvedValue(networkResponse)
global.caches.open.mockResolvedValue({ put: vi.fn() })
const result = await swModule.networkFirst({ request })
expect(result).toEqual(networkResponse)
})
it('should return cached response when network fails', async () => {
const request = new Request('/test')
const cachedResponse = new Response('cached content')
global.fetch.mockRejectedValue(new Error('Network error'))
global.caches.open.mockResolvedValue({
match: vi.fn().mockResolvedValue(cachedResponse),
})
const result = await swModule.networkFirst({ request })
expect(result).toEqual(cachedResponse)
})
})
})

14
vitest.config.ts Normal file
View file

@ -0,0 +1,14 @@
import { defineConfig } from 'vitest/config'
// Exclude generated files and heavy folders from the watcher to avoid
// continuous re-runs when build output or other tools touch files.
export default defineConfig({
test: {
globals: true,
environment: 'node',
exclude: ['dist/**', 'public/**', 'node_modules/**'],
},
watch: {
exclude: ['dist/**', 'public/**', 'node_modules/**', '.git/**'],
},
})