Compare commits

..

No commits in common. "main" and "v1.3.33" have entirely different histories.

38 changed files with 3171 additions and 2308 deletions

View file

@ -1,3 +1,3 @@
echo "post-commit..."
git push gh
git push sh
git push gh --mirror
git push sh --mirror

View file

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

View file

@ -28,7 +28,6 @@ $ pnpm i
| `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 |
## Deployment
@ -47,7 +46,7 @@ If you want to run build before deploying, do `npm run build:deploy`
## Auto-mirror to various code forges
I push this project to three different code forges, because I'm paranoid like that. No, I just like to show off. I do this with a post-commit hook that automatically calls `git push <repo> --mirror`, and you can see that in the `.husky/post-commit` file.
The power of git lies within its decentralized nature. I push this project to three different code forges, because I'm paranoid like that. No, I just like to show off. I do this with a post-commit hook that automatically calls `git push <repo> --mirror`, and you can see that in the `.husky/post-commit` file.
---

View file

@ -13,7 +13,7 @@ import GithubStats from 'astro-github-stats'
<p>Embed GitHub stats on your Astro page</p>
<ul>
<li>
GitHub repo: <a href="https://ayco.io/gh/astro-github-stats"
GitHub repo: <a href="https://github.com/ayoayco/astro-github-stats"
>astro-github-stats</a
>
</li>

View file

@ -2,7 +2,6 @@
import { defineConfig } from 'astro/config'
import serviceWorker from '@ayco/astro-sw'
import sitemap from '@astrojs/sitemap'
import { viteStaticCopy } from 'vite-plugin-static-copy'
import * as data from './package.json'
@ -12,8 +11,7 @@ import icon from 'astro-icon'
export default defineConfig({
site: 'https://ayo.ayco.io',
image: {
domains: ['cdn.bsky.app', 'media.ayco.io'],
remotePatterns: [{ protocol: 'https' }],
domains: ['cdn.bsky.app'],
},
integrations: [
sitemap(),
@ -22,10 +20,6 @@ export default defineConfig({
assetCachePrefix: 'ayco-personal-site',
assetCacheVersionID: data.version,
logAssets: true,
include: [
'/wc/node_modules/web-component-base/dist/index.js',
'/wc/node_modules/@ayo-run/status-indicator/dist/status-indicator.js',
],
esbuild: {
minify: true,
},
@ -43,20 +37,4 @@ export default defineConfig({
},
}),
],
vite: {
plugins: [
viteStaticCopy({
targets: [
{
src: './node_modules/web-component-base/dist/index.js',
dest: 'wc',
},
{
src: './node_modules/@ayo-run/status-indicator/dist/status-indicator.js',
dest: 'wc',
},
],
}),
],
},
})

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/config-helpers'
import { includeIgnoreFile } from '@eslint/compat'
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.78",
"version": "1.3.33",
"private": true,
"scripts": {
"astro": "astro",
@ -10,54 +10,49 @@
"build": "astro telemetry disable && astro build",
"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",
"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",
"predeploy": "npm version patch && npm run build",
"deploy": "npm run predeploy && eval $(grep '^HOST' .env) && scp -r dist ayo@$HOST:~/ayco.io-flask",
"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",
"test": "vitest run ."
"prep:now": "npx jiti ./commands/prep-now.js"
},
"devDependencies": {
"@astrojs/sitemap": "^3.7.2",
"@ayco/astro-sw": "^1.0.0",
"@eslint/compat": "^2.1.0",
"@eslint/config-helpers": "^0.6.0",
"@astro-reactive/form": "^0.10.1",
"@astro-reactive/validator": "^0.5.1",
"@astrojs/sitemap": "^3.7.0",
"@ayco/astro-sw": "^0.8.14",
"@eslint/compat": "^2.0.2",
"@eslint/js": "^10.0.1",
"@iconify-json/mdi": "^1.2.3",
"@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",
"@iconify-json/simple-icons": "^1.2.71",
"@iconify-json/tabler": "^1.2.27",
"@typescript-eslint/parser": "^8.56.0",
"astro": "^5.17.3",
"astro-eslint-parser": "^1.3.0",
"astro-github-stats": "^0.8.0",
"astro-icon": "^1.1.5",
"consola": "^3.4.2",
"eslint": "^10.4.0",
"eslint-plugin-astro": "^1.7.0",
"eslint": "^10.0.1",
"eslint-plugin-astro": "^1.6.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"globals": "^17.6.0",
"globals": "^17.3.0",
"husky": "^9.1.7",
"jiti": "^2.7.0",
"lint-staged": "^17.0.4",
"jiti": "^2.6.1",
"lint-staged": "^16.2.7",
"pathe": "^2.0.3",
"prettier": "^3.8.3",
"prettier": "^3.8.1",
"prettier-plugin-astro": "^0.14.1",
"rehype-stringify": "^10.0.1",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.2",
"tslib": "^2.8.1",
"typescript": "^6.0.3",
"typescript-eslint": "^8.59.3",
"unified": "^11.0.5",
"vite-plugin-static-copy": "^4.1.0",
"vitest": "^4.1.7",
"vitest-dom": "^0.1.1"
"typescript": "^5.9.3",
"typescript-eslint": "^8.56.0",
"unified": "^11.0.5"
},
"lint-staged": {
"*.{js,mjs,astro,ts}": [
@ -68,9 +63,5 @@
"prettier --write"
]
},
"packageManager": "pnpm@11.2.2",
"dependencies": {
"@ayo-run/status-indicator": "^2.1.2",
"web-component-base": "^4.1.2"
}
"packageManager": "pnpm@10.14.0"
}

File diff suppressed because it is too large Load diff

View file

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

Binary file not shown.

BIN
public/ayo-2025-sm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/ayo-2025.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

BIN
public/ayo-sm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -1,13 +0,0 @@
-----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

@ -1,137 +1,35 @@
Sitemap: https://ayo.ayco.io/sitemap-index.xml
User-agent: AddSearchBot
User-agent: AI2Bot
User-agent: AI2Bot-DeepResearchEval
User-agent: Ai2Bot-Dolma
User-agent: aiHitBot
User-agent: amazon-kendra
User-agent: *
Disallow:
User-agent: AdsBot-Google
User-agent: Amazonbot
User-agent: AmazonBuyForMe
User-agent: Amzn-SearchBot
User-agent: Amzn-User
User-agent: Andibot
User-agent: Anomura
User-agent: anthropic-ai
User-agent: Applebot
User-agent: Applebot-Extended
User-agent: atlassian-bot
User-agent: Awario
User-agent: AzureAI-SearchBot
User-agent: bedrockbot
User-agent: bigsur.ai
User-agent: Bravebot
User-agent: Brightbot 1.0
User-agent: BuddyBot
User-agent: AwarioRssBot
User-agent: AwarioSmartBot
User-agent: Bytespider
User-agent: CCBot
User-agent: Channel3Bot
User-agent: ChatGLM-Spider
User-agent: ChatGPT Agent
User-agent: ChatGPT-User
User-agent: Claude-SearchBot
User-agent: Claude-User
User-agent: Claude-Web
User-agent: ClaudeBot
User-agent: Cloudflare-AutoRAG
User-agent: CloudVertexBot
User-agent: Claude-Web
User-agent: cohere-ai
User-agent: cohere-training-data-crawler
User-agent: Cotoyogi
User-agent: Crawl4AI
User-agent: Crawlspace
User-agent: Datenbank Crawler
User-agent: DeepSeekBot
User-agent: Devin
User-agent: Diffbot
User-agent: DuckAssistBot
User-agent: Echobot Bot
User-agent: EchoboxBot
User-agent: DataForSeoBot
User-agent: FacebookBot
User-agent: facebookexternalhit
User-agent: Factset_spyderbot
User-agent: FirecrawlAgent
User-agent: FriendlyCrawler
User-agent: Gemini-Deep-Research
User-agent: Google-CloudVertexBot
User-agent: Google-Extended
User-agent: Google-Firebase
User-agent: Google-NotebookLM
User-agent: GoogleAgent-Mariner
User-agent: GoogleOther
User-agent: GoogleOther-Image
User-agent: GoogleOther-Video
User-agent: GPTBot
User-agent: iAskBot
User-agent: iaskspider
User-agent: iaskspider/2.0
User-agent: IbouBot
User-agent: ICC-Crawler
User-agent: ImagesiftBot
User-agent: imageSpider
User-agent: img2dataset
User-agent: ISSCyberRiskCrawler
User-agent: kagi-fetcher
User-agent: Kangaroo Bot
User-agent: KlaviyoAIBot
User-agent: KunatoCrawler
User-agent: laion-huggingface-processor
User-agent: LAIONDownloader
User-agent: LCC
User-agent: LinerBot
User-agent: Linguee Bot
User-agent: LinkupBot
User-agent: Manus-User
User-agent: meta-externalagent
User-agent: Meta-ExternalAgent
User-agent: meta-externalfetcher
User-agent: Meta-ExternalFetcher
User-agent: meta-webindexer
User-agent: MistralAI-User
User-agent: MistralAI-User/1.0
User-agent: MyCentralAIScraperBot
User-agent: netEstate Imprint Crawler
User-agent: NotebookLM
User-agent: NovaAct
User-agent: OAI-SearchBot
User-agent: magpie-crawler
User-agent: Meltwater
User-agent: omgili
User-agent: omgilibot
User-agent: OpenAI
User-agent: Operator
User-agent: PanguBot
User-agent: Panscient
User-agent: panscient.com
User-agent: Perplexity-User
User-agent: peer39_crawler
User-agent: peer39_crawler/1.0
User-agent: PerplexityBot
User-agent: PetalBot
User-agent: PhindBot
User-agent: Poggio-Citations
User-agent: Poseidon Research Crawler
User-agent: QualifiedBot
User-agent: QuillBot
User-agent: quillbot.com
User-agent: SBIntuitionsBot
User-agent: Scrapy
User-agent: SemrushBot-OCOB
User-agent: SemrushBot-SWA
User-agent: ShapBot
User-agent: Sidetrade indexer bot
User-agent: Spider
User-agent: TavilyBot
User-agent: TerraCotta
User-agent: Thinkbot
User-agent: TikTokSpider
User-agent: Timpibot
User-agent: TwinAgent
User-agent: VelenPublicWebCrawler
User-agent: WARDBot
User-agent: Webzio-Extended
User-agent: webzio-extended
User-agent: wpbot
User-agent: WRTNBot
User-agent: YaK
User-agent: YandexAdditional
User-agent: YandexAdditionalBot
User-agent: PiplBot
User-agent: Seekr
User-agent: YouBot
User-agent: ZanistaBot
Disallow: /

BIN
public/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 KiB

View file

@ -45,7 +45,7 @@ const year = new Date().getFullYear()
<p>
Copyright &#169; 2022-{year}
<a href="/">Ayo Ayco</a>. This website <a
href="https://v1.ayos.blog/stopped-tracking-on-my-sites"
href="https://ayos.blog/stopped-tracking-on-my-sites"
>does not track users</a
>. See the <a href="https://git.ayo.run/ayo/ayco.io-astro">source code.</a>
</p>

View file

@ -3,20 +3,17 @@ export interface Props {
title?: string | undefined
description?: string | undefined
ogImage?: string | undefined
ogFileType?: string | undefined
}
const defaultDescription =
'Professional software engineer specializing in web development with a decade of experience building web applications for both private businesses and government-funded high-impact projects utilizing web technologies, IoT, data viz/insights, remote sensing, and GIS'
const defaultTitle = 'Ayo Ayco - Tech Leader, Software Engineer, Web Developer'
const defaultOgImage = 'ayo.png'
const defaultOgFileType = 'image/png'
let {
title,
description = defaultDescription,
ogImage = defaultOgImage,
ogFileType = defaultOgFileType,
} = Astro.props
const baseURL = Astro.site?.toString().slice(0, -1) // ?? 'https://ayo.ayco.io'
@ -38,8 +35,7 @@ const baseURL = Astro.site?.toString().slice(0, -1) // ?? 'https://ayo.ayco.io'
<meta property="og:url" content={baseURL + Astro.url.pathname} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image:type" content={ogFileType} />
<meta property="og:image" content={ogImage} />
<meta property="og:image" content={`/${ogImage}`} />
<!-- Links -->
<link rel="canonical" href={baseURL + Astro.url.pathname} />
@ -54,19 +50,4 @@ const baseURL = Astro.site?.toString().slice(0, -1) // ?? 'https://ayo.ayco.io'
<link rel="icon" href="favicon.svg" />
<link rel="mask-icon" href="mask-icon.svg" color="#000000" />
<link rel="apple-touch-icon" href="apple-touch-icon.png" />
<!-- Web Components -->
<script type="importmap">
{
"imports": {
"web-component-base": "/wc/node_modules/web-component-base/dist/index.js",
"status-indicator": "/wc/node_modules/@ayo-run/status-indicator/dist/status-indicator.js"
}
}
</script>
<script type="module">
// importing is enough to register the elements
// eslint-disable-next-line
import StatusIndicator from 'status-indicator'
</script>
</head>

View file

@ -1,7 +0,0 @@
{
"title": "BACK AT IT",
"description": "",
"publishDate": "2026-02-22",
"publishedOn": "the 22nd day of February 2026",
"publishState": ""
}

View file

@ -1,9 +0,0 @@
Hello! Been a while!
Yes, I'm getting back to some of my shelved side projects like <a href="https://ayco.io/gh/mcfly#readme">McFly</a> and <a href="https://ayco.io/gh/astro-sw#readme">Astro SW</a>.
Why were they shelved, you ask?
Well, that's a story for another time.
You can <a href="https://ayco.io/gh">Follow me on GitHub</a> to see my activities on these projects if that's your thing. :)

View file

@ -1,7 +0,0 @@
{
"title": "Joining the Elk team, speaking my mind",
"description": "",
"publishDate": "2026-04-11",
"publishedOn": "a sunny 11th day of April",
"publishState": "in my home in Amsterdam"
}

View file

@ -1,23 +0,0 @@
Aaaaah summer feels today. Bugs all buzzing and
All the darkness are fading and people who lived most of their lives in the tropics but emigrated to less sunny places become friendlier and nicer.
BUT NOT ME! 😡
kidding. 😅
Anyway... I have [joined the team](https://github.com/elk-zone/elk/pull/3594) behind [Elk](https://elk.zone) where I continue to be the bottle of sunshine that I am.
Kidding again.
...where I try to be more annoying than usual and tag @everyone in our Discord server. That's it. That's me now.
Also I have been smothering my [threads](https://ayo.ayco.io/threads) page with my deepest darkest thoughts -- and people either like it or not. That's how it is really. It's probably gonna be written on my grave: "HIS THOUGHTS SUCK"
...though honestly I have been getting more responses of validation and new connections that I have a feeling maybe I have been touching on something.... true???
Only time will tell. As my therapist said: these thoughts will not matter in 5 years.
...because they will be deleted.
Kidding!

View file

@ -46,30 +46,25 @@ export const footerLinks: Link[] = [
icon: 'git',
},
{
text: 'GitHub',
url: 'https://ayco.io/gh',
icon: 'github',
},
{
text: 'CodePen',
url: 'https://codepen.io/ayo-run',
icon: 'codepen',
text: 'SourceHut',
url: 'https://sr.ht/~ayoayco',
icon: 'sourcehut',
},
{
text: 'Mastodon',
url: 'https://main.elk.zone/social.ayco.io/@ayo',
icon: 'mastodon',
},
{
text: 'Bluesky',
url: 'http://bsky.app/profile/ayo.run',
icon: 'bluesky',
},
{
text: 'Pixelfed',
url: 'https://metapixl.com/@ayo',
icon: 'pixelfed',
},
{
text: 'Bluesky',
url: 'http://bsky.app/profile/ayo.ayco.io',
icon: 'bluesky',
},
{
text: 'Instagram',
url: 'https://www.instagram.com/ayoayco/',
@ -80,6 +75,11 @@ export const footerLinks: Link[] = [
url: 'https://www.linkedin.com/in/ayoayco/',
icon: 'linkedin',
},
{
text: 'GitHub',
url: 'https://ayco.io/gh',
icon: 'github',
},
]
export const socialLinks: Link[] = [

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": "",
"title": "BACK AT IT",
"description": "",
"publishDate": "2026-02-22",
"publishedOn": "the 22nd day of February 2026",
"publishState": ""
}

View file

@ -1,13 +1,7 @@
I bought a pull-up bar, and in addition to a few barbells at home, I am starting to have a mini gym.
Hello! Been a while!
Super happy that I am getting healthier as I build my workout routines.
Yes, I'm getting back to some of my shelved side projects like <a href="https://git.ayo.run/ayo/mcfly#readme">McFly</a> and <a href="https://git.ayo.run/ayo/astro-sw#readme">Astro SW</a>.
To keep my mind sharp, I find it helpful to have several hobby things I can get back to from time to time.
Why were they shelved, you ask?
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!
Well, that's a story for another time.

View file

@ -8,34 +8,15 @@ import { links } from '../constants/links'
export interface Props {
title?: string
description?: string
ogImage?: string
}
const { title, description } = Astro.props
import { getImage } from 'astro:assets'
// fetch mastodon account
const response = await fetch(
'https://social.ayco.io/api/v1/accounts/lookup?acct=ayo'
)
const data = await response.json()
const { avatar } = data
const ogImage = await getImage({
src: avatar,
width: 400,
height: 400,
format: 'png',
})
const ogFileType = 'image/png'
const { title, description, ogImage } = Astro.props
---
<!doctype html>
<html lang="en">
<Head
title={title}
description={description}
ogImage={ogImage.src}
ogFileType={ogFileType}
/>
<Head title={title} description={description} ogImage={ogImage} />
<body class="h-card">
<Nav links={links} />
@ -127,10 +108,10 @@ const ogFileType = 'image/png'
}
</style>
<!-- Anonymous RUM for web perf by Cloudflare Web Analytics -->
<!-- <script
<script
defer
src="https://static.cloudflareinsights.com/beacon.min.js"
data-cf-beacon='{"token": "a39ad600e67a4db8960c639d2552435c"}'></script> -->
data-cf-beacon='{"token": "a39ad600e67a4db8960c639d2552435c"}'></script>
<!-- End Cloudflare Web Analytics -->
</body>
</html>

View file

@ -1,7 +1,3 @@
.invisible {
display: none;
}
small {
font-size: var(--font-size-sm);
}

View file

@ -19,26 +19,11 @@ import Footer from '../components/Footer.astro'
target="_blank">iSAQB</a
>.
</p>
<p>
I have contributed software development expertise to
<a href="http://infor.com">Infor</a>,
<a href="https://www.priva.com/">Priva</a>,
<a href="http://itc.uplb.edu.ph">UPLB</a>,
<a href="http://dost.gov.ph">DOST</a>, and various government-funded
projects such as
<a href="http://noah.up.edu.ph">Project NOAH</a> — which consistently
<a href="https://www.youtube.com/watch?v=LKrV6vtGZEA">saved lives</a>
from natural hazards in
<a href="https://www.officialgazette.gov.ph/programs/about-project-noah/">
the Philippines
</a>.
</p>
<p>
In my spare time, I find it fun building <a href="/showcase">projects</a>,
running self-hosted services at <a href="https://ayo.run">ayo.run</a>, and
volunteering to <a
href="https://v1.ayos.blog/why-fediverse/"
target="_blank">Fediverse</a
volunteering to <a href="https://ayos.blog/why-fediverse/" target="_blank"
>Fediverse</a
> projects like <a href="https://elk.zone">elk.zone</a>,
<a href="https://m.webtoo.ls/public/local">webtoo.ls</a>, and <a
href="https://m.webtoo.ls/@vitest">vitest's fedi presence</a
@ -51,11 +36,10 @@ import Footer from '../components/Footer.astro'
power of multi-generational & multi-cultural communities.
</p>
<h2 id="contact">Contact info</h2>
<p>My inbox is open to everyone.</p>
<ul>
<li>✉️ Email: <a href="mailto:ayo@ayco.io">ayo@ayco.io</a></li>
<li>
✉️ Email me: <a href="mailto:ayo@ayco.io">ayo@ayco.io</a> &middot
<small> (<a href="/pgp">PGP key</a>)</small>
👀 Fedi: <a href="https://social.ayco.io/@ayo">@ayo@ayco.io</a>
</li>
<li>
💬 Signal: <a
@ -63,15 +47,19 @@ import Footer from '../components/Footer.astro'
>ayo.88</a
>
</li>
<li>💬 Discord: ayo-run</li>
<li>💬 Matrix: @ayoayco:matrix.org</li>
<!--li>
💬 Discord: <a href="https://discord.gg/kkvW7GYNAp">Ayo's Projects</a>
</li-->
<li>💬 Discord: ayoayco</li>
</ul>
</main>
<Footer />
<style>
ul {
list-style: none;
padding-left: 0;
}
</style>
</Layout>
<style>
ul {
list-style: none;
padding-left: 0;
}
</style>

View file

@ -3,41 +3,34 @@ import Layout from '../layouts/Layout.astro'
import Card from '../components/Card.astro'
import Footer from '../components/Footer.astro'
import now from '../constants/now.json'
import { Picture } from 'astro:assets'
// fetch mastodon account
const response = await fetch(
'https://social.ayco.io/api/v1/accounts/lookup?acct=ayo'
)
const data = await response.json()
const { avatar, note } = data
const avatarSize = 150
---
<Layout>
<section class="highlighted-section">
<div class="highlighted-section__content">
<div class="invisible">
<div style="display:none">
<!-- h-card details -->
<span class="p-name">Ayo Ayco</span>
<a class="u-url u-uid" href="https://ayo.ayco.io">w</a>,
<section class="p-note" set:html={note} />
<p class="p-note">
Frontend guy who likes server adventures & works w/ linux. Looking to
specialize in web perf, but mostly feels like an impostor for now. Has
hobby projects.
</p>
</div>
<Picture
<img
class="u-photo highlighted-section__content__profile-picture"
alt="Ayo Ayco's avatar"
formats={['avif', 'webp']}
src={avatar}
width={avatarSize}
height={avatarSize}
src="/ayo-2025-sm.png"
width="140"
height="140"
/>
<div class="highlighted-section__content__text">
<h1 title="Ayo Ayco | Software Engineer + Web Developer">
Hi, I'm <span class="heavy-text">Ayo</span>!
</h1>
<a href="/now" class="now-wrapper action primary">
<!--a href="https://forms.ayo.run/form/tnz7FybY" class="now-wrapper"-->
<a href="/now" class="now-wrapper">
<span class="now-label">now</span>
<span class="status">{now.title}</span>
</a>
@ -50,6 +43,9 @@ const avatarSize = 150
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">
@ -61,18 +57,17 @@ const avatarSize = 150
/>
<Card
newTab={true}
href="/Ayco_Resume.pdf"
href="https://ayco.io/cv"
title="CV / Resume"
body="Download and peruse my skills and experience"
/>
</ul>
</section>
</main>
<Footer />
</Layout>
<style define:vars={{ avatarSize }}>
<style>
h1 {
margin: 0;
color: rgba(255, 255, 255, 0.75);
@ -83,30 +78,14 @@ const avatarSize = 150
color: white;
}
.action {
margin-right: 0.5rem;
.now-wrapper {
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 40px;
padding: 8px 4px;
font-weight: normal;
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 {
@ -146,8 +125,8 @@ const avatarSize = 150
background-color: var(--color-brand-blue-1);
display: block;
border-radius: 50%;
width: var(--avatarSize) px;
height: var(--avatarSize) px;
width: 140px;
height: 140px;
}
.highlighted-section__content ul a {
@ -206,7 +185,7 @@ const avatarSize = 150
}
@media only screen and (max-device-width: 360px) {
.action.primary {
.now-wrapper {
border: 0px;
border-radius: 0;
font-size: var(--font-size-sm);
@ -237,12 +216,6 @@ 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

@ -42,9 +42,10 @@ const description =
</p>
<p>
In my spare time, I build side projects for people I love, like <a
href="https://ayco.io/gh/mnswpr#readme"
href="https://github.com/ayoayco/mnswpr#readme"
target="_blank">mnswpr.com</a
> and <a href="https://ayco.io/gh/twists-and-shapes-and-turns#readme"
> and <a
href="https://github.com/ayoayco/twists-and-shapes-and-turns#readme"
>Kaboom!</a
>
</p>

View file

@ -1,70 +0,0 @@
---
import Layout from '../../../../layouts/Layout.astro'
import Footer from '../../../../components/Footer.astro'
const title = `BACK AT IT`
const description = ``
let publishedOn = `the 22nd day of February 2026`
const publishDate = `2026-02-22`
const publishState = ``
const content = `<p>Hello! Been a while!</p>
<p>Yes, I'm getting back to some of my shelved side projects like McFly and Astro SW.</p>
<p>Why were they shelved, you ask?</p>
<p>Well, that's a story for another time.</p>
<p>You can Follow me on GitHub to see my activities on these projects if that's your thing. :)</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>

View file

@ -1,77 +0,0 @@
---
import Layout from '../../../../layouts/Layout.astro'
import Footer from '../../../../components/Footer.astro'
const title = `Joining the Elk team, speaking my mind`
const description = ``
let publishedOn = `a sunny 11th day of April`
const publishDate = `2026-04-11`
const publishState = `in my home in Amsterdam`
const content = `<p>Aaaaah summer feels today. Bugs all buzzing and</p>
<p>All the darkness are fading and people who lived most of their lives in the tropics but emigrated to less sunny places become friendlier and nicer.</p>
<p>BUT NOT ME! 😡</p>
<p>kidding. 😅</p>
<p>Anyway... I have <a href="https://github.com/elk-zone/elk/pull/3594">joined the team</a> behind <a href="https://elk.zone">Elk</a> where I continue to be the bottle of sunshine that I am.</p>
<p>Kidding again.</p>
<p>...where I try to be more annoying than usual and tag @everyone in our Discord server. That's it. That's me now.</p>
<p>Also I have been smothering my <a href="https://ayo.ayco.io/threads">threads</a> page with my deepest darkest thoughts -- and people either like it or not. That's how it is really. It's probably gonna be written on my grave: "HIS THOUGHTS SUCK"</p>
<p>...though honestly I have been getting more responses of validation and new connections that I have a feeling maybe I have been touching on something.... true???</p>
<p>Only time will tell. As my therapist said: these thoughts will not matter in 5 years.</p>
<p>...because they will be deleted.</p>
<p>Kidding!</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>

View file

@ -1,124 +0,0 @@
---
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 */
document.querySelector('.copy-btn').addEventListener('click', async () => {
const keyText = document.querySelector('#public-key code').innerText.trim()
try {
await navigator.clipboard.writeText(keyText)
const btn = document.querySelector('.copy-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,17 +11,11 @@ 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://v1.ayos.blog/projects">my blog</a
See more of my previous projects at <a href="https://ayos.blog/projects"
>my blog</a
>.
</p>
<ul>
<Card
newTab
href="https://status-indicator.webcomponent.io/"
title="<status-indicator>"
body="Colored circles that can pulse"
/>
<Card
newTab
href="https://git.ayo.run/ayo/simple-tts#readme"
@ -30,13 +24,13 @@ import Card from '../components/Card.astro'
/>
<Card
newTab
href="https://git.ayo.run/ayo/mcfly#readme"
href="https://mcfly.js.org"
title="McFly"
body="A meta-framework for building web experiences"
/>
<Card
newTab
href="https://git.ayo.run/ayo/astro-sw#readme"
href="https://ayco.io/sh/astro-sw#readme"
title="Astro SW"
body="Integration to use your service worker with Astro"
/>
@ -48,28 +42,22 @@ import Card from '../components/Card.astro'
/>
<Card
newTab
href="https://git.ayo.run/ayo/astro-resume#readme"
href="https://ayco.io/sh/astro-resume#readme"
title="Astro Resume"
body="Utilities for serializing data from server for use in the client with types preserved across components."
/>
<Card
newTab
href="https://astro-reactive.js.org"
title="Astro Reactive"
body="Let your Data build your UI with Astro components 🔥"
/>
<Card
newTab
href="https://cozy.pub"
title="Cozy 🧸"
body="Your modern-day reading assistant"
/>
<Card
newTab
href="https://ayco.io/n/generate-timezone-json"
title="Time Zone JSON Generator"
body="Generate a JSON file containing time zones from the official IANA Database or your own zone.tab file"
/>
<Card
newTab
href="https://cozy.pub"
title="Cozy 🧸"
body="Your modern-day reading assistant"
/>
<Card
newTab
href="https://kaboom.ayco.io"
@ -82,6 +70,11 @@ import Card from '../components/Card.astro'
title="Minesweeper"
body="Recreated the classic game for the web 💣"
/>
<Card
href="/showcase/astro-reactive-form"
title="Reactive Form"
body="The reactive form component for Astro 🔥"
/>
</ul>
</main>
<Footer />

View file

@ -1,7 +1,4 @@
---
/**
* The code for /threads in production is in: https://git.ayo.run/ayo/threads
*/
import Layout from '../layouts/Layout.astro'
import Footer from '../components/Footer.astro'
@ -9,10 +6,6 @@ const page = {
title: "Ayo's Threads",
description: 'Incubator for thoughts before the become a blog',
}
console.warn(
'The code for /threads in production is in: https://git.ayo.run/ayo/threads'
)
---
<Layout title={page.title} description={page.description}>
@ -21,28 +14,6 @@ console.warn(
<p>{page.description}</p>
<p>Visit the <a href="https://ayco.io/threads">page</a></p>
<p>You're probably wondering how it works.</p>
<p>
What you are viewing now, is but a placeholder in the <a
href="https://git.ayo.run/ayo/ayco.io-astro#readme"
>static parts of my personal website.</a
> This is not the full picture. There is a <a
href="https://git.ayo.run/ayo/ayco.io-flask#readme">Flask server</a
> serving the static assets who is also responsible for attaching dynamic parts
/ blueprints like my <a href="https://git.ayo.run/ayo/threads#readme"
>Threads</a
> project.
</p>
<p>
I built it that way, because Python serves as a good "glue" to put
different parts together. Especially now with <em>LLMs</em>... where
Python is probably their greatest strength.
</p>
<p>
Anyway, for questions don't hesitate to <a
href="https://ayo.ayco.io/about#contact">contact me</a
>
</p>
</main>
<Footer />
</Layout>

View file

@ -1,121 +0,0 @@
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()
})
})

View file

@ -1,91 +0,0 @@
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)
})
})
})