Compare commits

...

352 commits

Author SHA1 Message Date
Ayo
d8d5b808b7 refactor(sw): add jsdoc 2025-08-26 21:37:13 +02:00
Ayo
4b67c33fe9 chore: separate scripts for build & preview 2025-08-26 21:36:56 +02:00
Ayo
cc3ad053e7 chore: add instrustions for purge job 2025-08-26 21:36:06 +02:00
Ayo
7b41a3831e chore: ignore .continue config 2025-08-24 11:20:20 +02:00
Ayo
c78c802061 chore: add docs source for Continue 2025-08-24 11:13:03 +02:00
Ayo
5039f9c6af refactor: catch potential error; add comments 2025-08-23 20:43:51 +02:00
Ayo
e751a4e5d3 fix: remove renamed component 2025-08-23 20:41:47 +02:00
Ayo Ayco
02bae8f0e3 feat: add an example systemd service 2025-08-22 09:29:48 +02:00
Ayo
825dff4c98 refactor: rename SimpleAddressBar to AddressBar 2025-08-17 21:55:20 +02:00
Ayo
b72e87ffd2 refactor: fix types 2025-08-08 21:32:23 +02:00
Ayo
8a0627f37c chore: remove TODO 2025-08-08 10:43:05 +02:00
Ayo
da2dea9323 feat: use redis.exists to check before using cached content 2025-08-08 10:34:39 +02:00
Ayo
d9fdf67c8a feat: update rate limits (25 max, within 5 minutes) 2025-08-07 19:23:33 +02:00
Ayo
4b46ba1ec3 feat: stricter rate limit 2025-08-07 18:59:15 +02:00
Ayo
290a13cc31 feat: set custom rate limit for 404s 2025-08-07 18:47:27 +02:00
Ayo
02cd6f4b5f feat: add rate limiter in fastify server 2025-08-07 18:45:14 +02:00
Ayo
a21401eab2 fix: prevent error on / 2025-08-07 17:46:04 +02:00
Ayo
065c972bd0 perf: skip fetching if url is empty or home 2025-08-07 17:43:32 +02:00
Ayo
4b3d4bca19 feat: prefix redis key with 'cozy:url:' 2025-08-07 17:40:29 +02:00
Ayo
1c7ca35d85 feat: use Redis as simple cache for fetched articles 2025-08-07 17:31:15 +02:00
Ayo
5c17784ed5 chore: update deps; add build server script 2025-08-07 12:01:59 +02:00
Ayo
79852df5df feat: initial docker build files 2025-08-07 12:00:21 +02:00
Ayo
13bc88a4fe chore: various improvements 2025-06-26 21:51:48 +02:00
8949b155d7 chore: em-dash for literary geniuses 2025-05-30 18:43:14 +02:00
1c6229b802 chore: update ways to get in touch on the readme 2025-04-27 19:04:15 +02:00
dd2f4fc0bb test: fix failing test for cozify 2025-04-27 17:29:16 +02:00
b3b8777a39 fix: old cached articles with cozified links going to an a route 2025-04-26 17:49:38 +02:00
2cf0e53a47 feat: release new client assets 2025-04-26 17:05:04 +02:00
63d26f8c6c feat: set vite config server.fs.strict to false 2025-04-20 18:56:41 +02:00
50108b604f chore(deps): update dependencies 2025-04-20 18:56:24 +02:00
41a76aa6ee fix: remove unused prop 2025-03-05 22:49:02 +01:00
6aca873c75 feat: use astro-resume 2025-03-05 22:46:59 +01:00
2358ec6d6f feat: update meta site_name 2025-03-02 21:18:34 +01:00
1a69da8af6 feat: update meta info 2025-03-02 21:15:05 +01:00
02329a5281 feat: update app meta info 2025-03-02 21:11:11 +01:00
6bf6a6eea8 feat: add og image 2025-03-02 21:04:28 +01:00
b2868f8691 chore: add section for bug reporting & contribution on the readme 2025-03-02 10:51:28 +01:00
85188e24b7 style: remove padding on main-content on small screens 2025-03-02 10:36:25 +01:00
1f92b549b0 feat: increment cache name 2025-03-02 10:33:45 +01:00
449d1e8f0e fix: what about on small screens you 2025-03-02 10:32:42 +01:00
94dfdd2ab9 feat: increment cache name 2025-03-02 09:46:29 +01:00
299fd89b5c fix: make children respectful 2025-03-02 09:41:23 +01:00
b564091093 feat: update touch icons 2025-02-15 20:30:05 +01:00
16b7ee4786 feat: increment version 2025-02-15 20:23:32 +01:00
4c6265d9cf feat: set icons 2025-02-15 20:21:09 +01:00
81e73b7f5e chore: update deps 2025-02-14 20:39:12 +01:00
6e95e5573b chore: remove Links section in readme 2025-02-09 11:49:56 +01:00
75ddf412ad chore: update sh note on readme 2025-02-08 20:32:47 +01:00
a87cd693d7 chore: add links to sourcehut on the readme 2025-02-08 20:16:14 +01:00
f576cf60a7 chore: add links to readme 2025-02-08 20:03:18 +01:00
49e78e6eb0 chore: update PR template 2025-02-08 19:49:56 +01:00
699f2f76be chore: add PR template pointing to SourceHut 2025-02-08 19:47:51 +01:00
73f5d5ef2b chore: update secret UUID 2025-02-08 19:41:37 +01:00
2b3e6cd63e chore: add automated push mirror 2025-02-08 19:40:17 +01:00
6fa235a13c chore: disable telemetry in build script 2025-01-27 22:01:59 +01:00
f6bae8d43c fix(blog): slug should be id 2025-01-11 10:56:41 +01:00
113248b6ed chore: migrate to Astro 5 stuff 2025-01-10 12:05:13 +01:00
Ayo Ayco
792b592fca chore: add dep lint-staged 2024-12-30 14:37:29 +01:00
Ayo Ayco
dcf5afcbec chore: only use astroswglobals in sw.mjs 2024-12-30 14:36:40 +01:00
cf0e3893df chore: set up eslint-plugin-prettier 2024-12-27 23:21:13 +01:00
20d25cc282 chore: test precommit 2024-12-27 21:32:43 +01:00
7baf073e5d chore: test precommit 2024-12-27 21:31:35 +01:00
40631874d8 chore: test precommit 2024-12-27 21:25:45 +01:00
b384210f1b chore: test precommit 2024-12-27 21:18:36 +01:00
6fa8e2124c chore: reuse gitignore patterns as eslint ignore 2024-12-27 14:51:36 +01:00
091b53e940 chore: update .prettierignore 2024-12-26 00:37:32 +01:00
cbd09d478f chore: remove bkup file 2024-12-26 00:16:36 +01:00
49cafdd6d5 fix: jsx fragment wrap 2024-12-26 00:15:31 +01:00
99315e639a chore: format code 2024-12-26 00:10:13 +01:00
d21e743569 chore: format .svg 2024-12-26 00:09:28 +01:00
8f9d5cf48f feat: use jumbotron.svg 2024-12-26 00:08:29 +01:00
f8d94bb2ee chore: add dev script 2024-12-26 00:04:49 +01:00
Ayo Ayco
95329ff66d chore: set up prettier, eslint 2024-12-25 23:58:30 +01:00
Ayo Ayco
38addd30bf chore: setup husky, eslint, prettier 2024-12-25 23:03:50 +01:00
249caabd67 refactor: remove logger 2024-11-10 21:01:35 +01:00
a567a88540 chore: move to new /test dir 2024-11-10 20:09:05 +01:00
ec02a26795 test(sanitizer): update test description 2024-11-10 20:07:25 +01:00
4e9a690e68 chore: add test script 2024-11-10 20:07:10 +01:00
d5b373df48 test(sanitizer): add tests for cozify() 2024-11-10 20:05:39 +01:00
ace0f6a4bf chore(deps): add vitest 2024-11-09 23:39:01 +01:00
Ayo Ayco
a70c6700bf chore(deps): astro & integrations upgrade 2024-10-27 20:08:47 +01:00
af22682662 fix: remove link for missing .css file 2024-10-06 10:55:20 +02:00
6a02a26571 chore: update version 2024-09-07 13:53:50 +02:00
e6cfd30e44 style: make app with 100% 2024-09-07 13:53:20 +02:00
5d199819c6 chore: remove @ayco/astro-resume from readme credits 2024-09-04 20:20:31 +02:00
13065bdec3 style: spacing adjustments 2024-09-04 20:14:00 +02:00
ebf7baa1ed refactor: rename layouts for App & Blog 2024-09-04 20:09:20 +02:00
aa271c8050 style: app spacing, color adjustments 2024-09-04 20:09:20 +02:00
d4bc2755bc refactor: various improvements
- use semantic elements (main, header, article)
- remove unused AddressBar component
- bundle article css
2024-09-04 20:09:20 +02:00
d4fef25400 refactor: remove @ayco/astro-resume 2024-09-04 20:09:20 +02:00
4a6dc800ca feat: add logos & use as favicon 2024-09-04 20:09:20 +02:00
f85608bc7c style: safe center home app 2024-09-03 23:13:06 +02:00
cc63223742 style: remove centering the home app 2024-09-03 22:47:30 +02:00
b60e459db0 0.3.3 2024-09-03 22:06:59 +02:00
9ca2cecec6 chore: assistant -> companion 2024-09-03 22:06:22 +02:00
715bf6f3a8 chore: fix readme logo alignment 2024-09-03 22:05:29 +02:00
2d56fa01b8 chore: update readme & app version 2024-09-03 22:03:48 +02:00
Ayo Ayco
37d05e3ec7
style: vertically center app home (#111)
* style: app-wrapper positioning to vertical center when on home
* refactor: undo unneeded change
* refactor: remove app.css
2024-09-03 21:52:25 +02:00
Ayo Ayco
bcba01f04c
style: address bar color adjustments (#110) 2024-09-03 21:28:17 +02:00
Ayo Ayco
9214b5eebd
feat: disappearing branding (#109) 2024-09-02 13:31:05 +02:00
Ayo Ayco
ce7941179b
refactor: remove sass (#108) 2024-09-02 11:34:33 +02:00
280554beae chore: update readme screenshot 2024-09-02 10:21:12 +02:00
196b834e02 chore: update version 2024-09-02 09:49:59 +02:00
Ayo Ayco
9238933b78
feat: add logo svg (#107) 2024-09-02 09:42:57 +02:00
67304e70f0 style: address bar focus glow 2024-09-02 01:12:53 +02:00
f09b2ad499 style: address bar focus outline accent 2024-09-02 01:12:53 +02:00
e717f20ff3 style: spacing adjustments 2024-09-02 01:12:53 +02:00
928314c151 chore: update version 2024-09-02 01:12:53 +02:00
1ee85644d6 style: jumbotron spacing 2024-09-02 01:12:53 +02:00
dd2613598e style: addressbar border lighter color 2024-09-02 01:12:53 +02:00
3bc3055dda style: footer and jumbotron text color 2024-09-02 01:12:53 +02:00
9eb06935e6 feat: Jumbotron & SimpleAddressBar 2024-09-02 01:12:53 +02:00
397e75c250 chore: update deps 2024-09-02 00:05:28 +02:00
a85bc511ac style: link colors in footer 2024-09-01 19:02:30 +02:00
7f97bd8287 chore: update domain to cozy.pub 2024-09-01 11:01:32 +02:00
0299d732dc chore(deps): update astro, mdx, check, astro-sw 2024-08-24 21:21:28 +02:00
e84ac825f0 feat: improve caching
- if addAll failed, show helpful log
- only cache if response.ok
- use more async by removing unnecessary awaits
2024-08-23 12:26:53 +02:00
8a4ef035fc chore: add comment 2024-08-21 09:12:56 +02:00
3bd2082c4e feat: remove preload fallback & fix logError missing 2024-08-19 17:13:25 +02:00
b18ea923e2 feat: do cache revalidation when it actually exists 2024-08-19 12:42:50 +02:00
8cc7a64338 chore: remove afterRegistration logging 2024-08-19 12:42:15 +02:00
85174feca2 chore: update version 2024-08-19 12:31:35 +02:00
28abc2bc6e fix: sw held back by network request for updating cached assets 2024-08-19 12:31:07 +02:00
985dcb15e8 feat: don't abort signal on network fetch 2024-08-18 19:49:35 +02:00
e7091d1514 feat: add log context 2024-08-18 19:47:27 +02:00
4751ce9b80 feat: delete old app assets caches; don't use date as version 2024-08-18 19:30:24 +02:00
91791b37b5 debug: use registrationHooks to log registered SWs 2024-08-18 12:36:18 +02:00
bb8f2c3cbd feat: use registrationHooks to log registered SWs 2024-08-18 12:08:34 +02:00
49e9c30663 debug: remove logging 2024-08-18 11:03:26 +02:00
500d270858 debug: log refreshing block 2024-08-18 11:00:57 +02:00
797f01d9d7 debug: log fetch 2024-08-18 10:55:49 +02:00
10dc28a76f debug: log new sw 2024-08-18 10:54:49 +02:00
bda88ca138 debug: log old sw 2024-08-18 10:52:59 +02:00
a967115ab9 feat: add debug safari not updating sw 2024-08-18 10:43:41 +02:00
91067beb40 chore: forceLogging on prd 2024-08-18 10:20:32 +02:00
9285372b26 fix: blog link 2024-08-17 22:26:38 +02:00
f43734eb9d chore: move meta post back to draft 2024-08-17 22:09:55 +02:00
d064d559da chore: update readme 2024-08-17 22:04:27 +02:00
2d0c74fc1e feat: do not revalidate every first load 2024-08-17 21:56:13 +02:00
cf065e3901 feat(blog): new meta post; update width snap 2024-08-17 21:44:41 +02:00
744067d211 chore: use mjs for sw 2024-08-17 21:35:36 +02:00
f79e918606 chore(deps): update @ayco/astro-sw 2024-08-17 21:35:21 +02:00
8d8b7df5b2 chore(deps): update @ayco/astro-sw 2024-08-17 12:50:32 +02:00
79e65f8994 feat: use new astro-sw support for typescript 2024-08-17 10:54:26 +02:00
d66845cb30 feat(blog): new draft posts 2024-08-16 17:48:48 +02:00
cc5f145ff0 feat: style changes 2024-08-16 17:47:59 +02:00
fe12530680 feat: new CurrentVersion component; adjust styles 2024-08-16 16:47:38 +02:00
bc55f3f39e feat: add a 'not found' page 2024-08-16 15:53:00 +02:00
9456a11732 style(blog): primary btn spacing & nav width 2024-08-16 15:52:43 +02:00
8ef4c272fc feat: add email buttons 2024-08-16 15:52:07 +02:00
4b5bfef321 fix: favicon meta 2024-08-16 15:50:54 +02:00
c133125039 chore(deps): update astro-sw 2024-08-16 15:50:05 +02:00
5442a3347b chore: add site config for sitemap 2024-08-16 15:49:43 +02:00
c54ba4b995 chore: update favicon & blog meta title 2024-08-16 15:36:02 +02:00
37302c31eb feat(blog): quivering quacks post 2024-08-16 12:00:05 +02:00
bb3c99023e feat(blog): update social link labels 2024-08-16 11:20:28 +02:00
534bfcb42d 0.3.2 2024-08-16 10:43:26 +02:00
75fe8b96d2 feat(blog): a11y & various style improvements; add author 2024-08-16 10:30:20 +02:00
9bddaef73b feat: set forceLogging to false 2024-08-16 08:54:09 +02:00
ff82604983 chore: remove !!! from logger 2024-08-16 08:49:08 +02:00
a8b7714412 chore: update cache version id 2024-08-16 08:48:24 +02:00
4daa099b22 feat: create logger; update cached articles on load 2024-08-16 08:47:31 +02:00
ed2364ebe8 feat(cozy): revalidate cached article 2024-08-16 07:34:15 +02:00
8d95361e88 chore(dep): update astro-sw 2024-08-15 21:46:35 +02:00
2734371e1c chore(deps): update astro & astro-sw 2024-08-15 21:18:00 +02:00
b0bcc57caa perf: improve caching strategy conditions 2024-08-15 17:35:36 +02:00
591671ef82 fix: prevent fetch duplication & use preloadResponsePromise 2024-08-15 16:25:25 +02:00
ec92421d2b feat: implement stale-while-revalidate caching strategy 2024-08-15 16:14:55 +02:00
e7ec5fafbf feat: timedout network first caching strategy 2024-08-15 15:30:41 +02:00
160110c2f2 feat: log cached url only 2024-08-14 21:19:25 +02:00
3299b904ac feat: update logging message 2024-08-14 21:08:15 +02:00
fe4518e132 chore(deps): update lock file 2024-08-14 21:04:32 +02:00
22ae5415d3 chore: new cache version id 2024-08-14 20:30:58 +02:00
ac1cf5df91 feat: don't open cozy in new tab 2024-08-14 20:27:54 +02:00
f0a51464be style(blog): nav links zero padding-left 2024-08-14 20:23:00 +02:00
566a6c2174 style(blog): fix layout to show full nav on mobile 2024-08-14 20:14:14 +02:00
99d7fea607 chore: replace deploy scripts with deploy:clien 2024-08-14 19:51:58 +02:00
d89047bdfa refactor(blog): description logic 2024-08-14 19:51:32 +02:00
88bf59ce27 chore: add deploy:blog script 2024-08-14 19:43:27 +02:00
13371aa8d3 style(blog): first blog item image max width 2024-08-14 19:43:14 +02:00
22faeb32df feat(blog): add/improve meta info 2024-08-14 19:34:48 +02:00
37c37b4f4b fix: use correct cache version 2024-08-14 18:58:55 +02:00
fb1f1e7cf6 refactor: tryCache function 2024-08-14 18:43:51 +02:00
9fef0dd913 feat: add unique build indicator 2024-08-14 18:43:01 +02:00
1d4cb25c4e feat: change Follow -> Mastodon 2024-08-14 17:55:04 +02:00
Ayo Ayco
35b415c508 Merge branch 'main' of github.com:ayoayco/cozy into main 2024-08-14 17:54:20 +02:00
b32ae5bc06 feat: remove test change 2024-08-14 17:54:18 +02:00
1620a0d595 feat: add test change 2024-08-14 17:54:18 +02:00
0cd0511d02 feat: change caching strategy to network-first 2024-08-14 17:54:18 +02:00
15bcc19f08 feat: add follow link to footer 2024-08-14 17:54:18 +02:00
b41cdd08ea feat: remove test change 2024-08-14 17:53:22 +02:00
480f2c94b0 feat: add test change 2024-08-14 17:51:31 +02:00
65ca1ac9ee feat: change caching strategy to network-first 2024-08-14 17:46:38 +02:00
24294f36fa feat: add follow link to footer 2024-08-14 17:17:49 +02:00
7ddf19db64 feat: link to original blog 2024-08-14 16:23:46 +02:00
fd20540869 chore: format 2024-08-14 16:20:44 +02:00
25d04c7868 style: adjust mobile screen width 2024-08-14 16:20:39 +02:00
608656664b feat: move primary button to social-links 2024-08-14 16:19:57 +02:00
58128c925b feat: first post; ssr index 2024-08-14 15:35:48 +02:00
ad7f30bb17 feat: initial /blog site 2024-08-14 14:56:01 +02:00
ad265a4d71 feat: use astro-sw automatic service worker registration 2024-08-13 20:44:48 +02:00
42b47da504 feat: activated update sw 2024-08-04 13:01:31 +02:00
d6f0770a2a chore: add @ayco/astro-sw in credits 2024-08-03 09:40:32 +02:00
07ee133ec7 refactor: move service-worker out of utils 2024-08-03 09:30:16 +02:00
2291ecda6a chore: made dev deps 2024-08-03 09:28:45 +02:00
ec60ebf87c chore: update sw.js docs 2024-08-03 09:15:53 +02:00
bc92382bf7 chore: update @ayco/astro-sw integration 2024-08-03 09:13:33 +02:00
9836972fb0 0.3.1 2024-08-03 09:00:02 +02:00
38db5a26e9 feat: use published @ayco/astro-sw 2024-08-03 08:58:57 +02:00
881c946a0a refactor: config prop rename serviceWorkerPath -> path 2024-08-03 01:10:00 +02:00
ea673e6bdd feat: implement assetCachePrefix & set sw config defaults 2024-08-03 01:01:58 +02:00
6562f35c0c chore: add to-do 2024-08-02 23:21:17 +02:00
612b7e64b4 refactor: cozy-build -> astro-sw astro integration 2024-08-02 23:17:21 +02:00
dca154b6fa chore: format astro config 2024-08-02 22:27:32 +02:00
c5a3b85390 refactor: move sw & cozy-build into src/plugins 2024-08-02 22:22:59 +02:00
fb2c3ce927 chore: update readme after v0.3.0 release 2024-08-02 17:52:49 +02:00
76cf64e9e3 0.3.0 2024-08-02 17:49:56 +02:00
799ac18c89 feat: refactor astro build integration & implement cache versioning 2024-08-02 17:43:08 +02:00
391cfe628b feat: remove unregister logic 2024-08-02 17:26:53 +02:00
fc9684fa99 feat: dynamically fill up initial cache with generated assets 2024-08-02 17:09:20 +02:00
e25fa1a941 feat: unregister sw for now 2024-08-02 16:30:33 +02:00
f42b1b313c feat: don't cache missing file 2024-08-02 16:20:44 +02:00
Ayo Ayco
c2ab1fc2c6
feat: add simple service worker (#98) 2024-08-02 15:34:47 +02:00
7ad30da26f feat: use favicon.ico 2024-08-02 15:21:11 +02:00
27daed78e8 chore: move favicon.ico to public dir 2024-08-02 15:19:04 +02:00
6dc2740eee Merge branches 'main' and 'main' of github.com:ayoayco/cozy 2024-08-02 15:12:49 +02:00
2b96b2827a 0.2.5 2024-07-27 21:36:52 +02:00
fbd9fd2224 chore: update deps 2024-07-27 21:36:45 +02:00
Ayo Ayco
0c199d8476
chore: Update README.md 2024-07-27 15:20:23 +02:00
2a6939694e chore: mention fastify & nginx on readme 2024-07-27 12:07:37 +02:00
f3f423dcfa chore: remove warning on readme 2024-07-27 12:05:19 +02:00
340027c0d8 chore: update readme 2024-07-27 11:58:50 +02:00
Ayo Ayco
0e05239059 feat: make server.mjs executable on unix 2024-07-27 11:56:33 +02:00
990fa4201f chore: ignore package-lock 2024-07-27 11:43:11 +02:00
4d0772a9ae feat: remove fastify-cli 2024-07-27 11:42:41 +02:00
Ayo Ayco
e8c5090d3d chore: add fastify-cli 2024-07-27 11:39:22 +02:00
892b030a01 feat: add build-preview script 2024-07-27 11:32:22 +02:00
Ayo Ayco
951e11f242 feat: add temporary landing page 2024-07-27 08:53:08 +02:00
d3abab1efc feat: build as middleware & use fastify 2024-07-27 08:43:58 +02:00
173d584d80 feat: add deploy script 2024-07-27 08:27:18 +02:00
4884f6a199 chore: ignore .astro/ 2024-07-27 08:03:36 +02:00
af47310a93 feat: switch from vercel -> node SSR 2024-07-27 07:57:42 +02:00
Ayo
8762f4c856 chore: remove funding.yml 2024-03-11 16:21:56 +01:00
Ayo
b28ea9e768 chore: migrate to pnpm 2024-03-09 20:36:10 +01:00
Ayo
6f7773a101 0.2.4 2024-03-09 11:48:44 +01:00
Ayo
88113b039a 0.2.3 2024-03-09 11:48:11 +01:00
Ayo
11dfdb99ea chore: update package hopage url 2024-03-09 11:47:58 +01:00
Ayo
8390c610d8 chore: update readme about deployment outage 2024-03-09 11:46:59 +01:00
Ayo Ayco
9c1f215bf7
chore: remove discord link -- I wanna transition to Matrix 2023-11-21 13:44:48 +01:00
Ayo Ayco
bcecb583e9
chore: remove discord link -- I wanna transition to Matrix 2023-11-21 13:43:47 +01:00
2c7c1dd208
Update README.md 2023-10-21 09:27:41 +02:00
Ayo Ayco
e7d27972b2
chore: update README.md 2023-10-15 01:46:22 +02:00
Ayo Ayco
fa2e186e23
chore: update README.md 2023-10-09 10:37:54 +02:00
Ayo
66fdb610f7 chore: update bookmarklet value 2023-10-06 21:38:15 +02:00
Ayo Ayco
da643ae46e
devops: move deployment to vercel (#93)
* devops: add vercel integration

* chore: announce move of deployment

* chore: update readme about move to netlify
2023-10-06 20:46:06 +02:00
Ayo
ac32f9e7fb chore: update CONTRIBUTING 2023-10-01 10:20:26 +02:00
Ayo Ayco
46b9c841e0
Update README.md 2023-10-01 10:19:45 +02:00
Ayo
ab5d0b6f99 chore: update readme 2023-10-01 09:50:40 +02:00
Ayo
41e6dde3c6 chore: update readme 2023-10-01 09:47:24 +02:00
Ayo
a9aa937ffa 0.2.2 2023-10-01 09:38:39 +02:00
Ayo
a104feb3f8 chore: revert minor 2023-10-01 09:38:24 +02:00
Ayo
f5d98a4dcb 0.3.0 2023-10-01 09:36:37 +02:00
Ayo
45faa00c43 feat: cozify cached articles 2023-10-01 09:36:21 +02:00
Ayo
99d3400c59 0.2.1 2023-10-01 09:18:43 +02:00
Ayo
1bb9bcb18a chore: update readme 2023-10-01 09:18:33 +02:00
Ayo
e3fdec5d15 0.2.0 2023-10-01 09:16:13 +02:00
Ayo
c005b5e8ae chore: update scripts for publishing 2023-10-01 09:16:07 +02:00
Ayo
b18ef7a5d7 chore: add supported engine only 18 and above 2023-10-01 09:01:18 +02:00
Ayo
9c1627b3e5 feat: cozify to prefetch links and open in cozy 2023-10-01 08:48:33 +02:00
Ayo
8470e5bee6 chore: update deps; add ultrahtml dep 2023-10-01 08:36:48 +02:00
Ayo
be13713a49 chore: update COC contact info 2023-09-29 22:27:58 +02:00
Ayo Ayco
f5bbd02f1f chore: update readme
- used "this project" instead of "I am" to indicate the project is a
  separate thing
2023-08-23 22:37:52 +02:00
Ayo Ayco
9fd8eba7c5 fix: settings popover not showing
- removed conflict of IDs
- removed home btn getting disabled
2023-08-23 22:32:31 +02:00
Ayo
b357ee58c4 fix: submit button not enabled 2023-08-16 15:37:53 +02:00
Ayo
b9406f006a 0.1.32 2023-08-15 23:08:45 +02:00
4c3ddc44ec
Merge pull request #79 from ayoayco/feat/18-use-cached-home-page
feat: progressively enhanced home link
2023-08-15 23:08:13 +02:00
Ayo
cc36a859d3 feat: progressively enhanced home link
- global app config via serializer/deserializer
- home link becomes router link if js enabled
2023-08-15 23:07:19 +02:00
Ayo Ayco
03543a30d3
chore: update README.md 2023-08-01 13:50:57 +02:00
Ayo
1c90ab9f2e chore: update readme and package info 2023-07-28 10:23:46 +02:00
Ayo
460aa22f6a refactor: use astro-resume 2023-07-28 10:06:46 +02:00
Ayo
a97106e263 refactor: Feature type 2023-07-28 10:06:46 +02:00
Ayo
062caab553 chore: update deps 2023-07-28 10:06:46 +02:00
Ayo
ad62e9ee0c feat: disable hide-images feature 2023-07-27 18:42:04 +02:00
Ayo
8eff875340 refactor: feature flags 2023-07-27 18:42:04 +02:00
Ayo
3b2a863b6c fix: feature flag checkbox name 2023-07-25 23:44:42 +02:00
Ayo
5ef3548ecf 0.1.31 2023-07-25 23:31:03 +02:00
Ayo
2cb23529ef feat: dynamic checkbox id for settings 2023-07-25 23:29:56 +02:00
Ayo
74c501a7bd feat: hide settings popover by default 2023-07-25 23:26:09 +02:00
Ayo
9b2f656a6a feat: settings popover expainer; set feature flags to false 2023-07-25 23:23:12 +02:00
Ayo
e86d9bc04c feat: implement feature flags 2023-07-25 23:23:12 +02:00
Ayo
1a36871616 feat: hide buttons if js disabled 2023-07-25 23:23:12 +02:00
Ayo
54a91b51d6 style: settings popover spacing 2023-07-25 23:23:12 +02:00
Ayo
8bcff3268f feat: temporary js solution for close/open 2023-07-25 23:23:12 +02:00
Ayo
8339b2b4ea feat: popover close icon 2023-07-25 23:23:12 +02:00
Ayo
d794633ed5 feat: initial SettingsPopover 2023-07-25 23:23:12 +02:00
Ayo
4a41624015 refactor: use .btn class; use querySelector
- use .btn class as selector to all clickable icons
- use querySelector for selecting icons
2023-07-25 23:23:12 +02:00
Ayo Ayco
f6e8ae9453 chore: update deps 2023-07-25 23:23:12 +02:00
Ayo Ayco
bad650e12b feat: initial settings toggle 2023-07-25 23:23:12 +02:00
Ayo
6326eafc34 chore: add FUNDING.yml 2023-07-21 18:37:37 +02:00
Ayo Ayco
727dbabceb
chore: update README.md 2023-07-18 09:31:56 +02:00
Ayo
ffe30bd7dd 0.1.30 2023-07-17 17:31:47 +02:00
Ayo
6022a0bc2d chore: update @ayco/astro-resume 2023-07-17 17:22:56 +02:00
Ayo
470152ea32 refactor: use Astro.props directly for @ayco/astro-resume 2023-07-16 22:05:34 +02:00
Ayo
2e574e289f refactor: update usage of @ayco/astro-resume 2023-07-16 22:01:16 +02:00
Ayo
a8b5513f00 feat: strictly typed resumed preferences 2023-07-16 20:24:34 +02:00
Ayo Ayco
664d81d86b feat: use @ayco/astro-resume 2023-07-16 13:19:37 +02:00
Ayo Ayco
20ae929267 update CONTRIBUTING 2023-07-16 11:07:09 +02:00
Ayo
70a2fb856f style: readability improvements 2023-07-02 21:22:48 +02:00
Ayo Ayco
4cbd95d911
refactor: minor clean up (#71)
* remove unnecessary leftovers

* add comment
2023-06-30 12:57:25 +02:00
Ayo
8dc528b39c feat: get last url param 2023-06-26 14:17:26 +02:00
Ayo Ayco
34e32abe51
fix: prevent history mess (#70)
* chore: add favicon

* fix: home article url

* fix: prevent duplicated history entry (unencoded & encoded)

* fix: prevent own app

* remove console.log

* fix: prevent showing cached cozy
2023-06-26 14:10:51 +02:00
Ayo
98b538396b 0.1.29 2023-06-24 22:39:44 +02:00
Ayo Ayco
7c67e674e5
refactor: lots of improvements (#69) 2023-06-24 22:32:20 +02:00
Ayo
f8e07c3195 style: spacing and text sizes 2023-06-24 11:30:37 +02:00
Ayo
5b091337cf style: spacing 2023-06-24 11:09:06 +02:00
Ayo
0ae558c43c 0.1.28 2023-06-24 11:04:18 +02:00
Ayo Ayco
c7910ee2a3
Merge pull request #65 from ayoayco/style/footer-improvements
style: footer improvements
2023-06-24 11:03:29 +02:00
Ayo
bd100977ab style: footer improvements 2023-06-24 11:01:40 +02:00
d8061e2cb5
Merge pull request #64 from ayoayco/feat/62-code-block-styling
style: code block
2023-06-24 10:18:49 +02:00
Ayo
fe3e054fda 0.1.27 2023-06-24 10:18:19 +02:00
Ayo
52507d0b41 style: code block 2023-06-24 10:17:54 +02:00
Ayo
4e26302101 feat: address bar improvements 2023-06-19 16:43:50 +02:00
Ayo
9d11dc8050 chore: update readme screenshot 2023-06-19 12:52:48 +02:00
Ayo
605169d463 0.1.26 2023-06-19 10:15:32 +02:00
361754fbb4
Merge pull request #59 from ayoayco/feat/home-link
feat: home link
2023-06-19 10:15:01 +02:00
Ayo
ea6b0d7658 feat: home link 2023-06-19 10:14:31 +02:00
Ayo
e51bb46f48 style: max width for footer 2023-06-18 18:30:39 +02:00
Ayo Ayco
55d69661b3
chore: update README.md 2023-06-18 14:44:11 +02:00
Ayo
d0954e387f feat: footer improvements 2023-06-18 13:25:19 +02:00
Ayo
c7e48939c4 feat: footer improvements 2023-06-15 17:01:30 +02:00
Ayo
f512731cab feat: require url input 2023-06-15 13:15:14 +02:00
Ayo
d4ff337844 feat: update document.title on navigation 2023-06-15 13:03:49 +02:00
Ayo Ayco
4470a4252c
chore: update README.md 2023-06-15 12:46:42 +02:00
Ayo Ayco
76e92ddee9
feat: address bar improvements (#55)
* feat: address bar UX improvements

* 0.1.25
2023-06-15 11:42:46 +02:00
Ayo
5b77b721f0 chore: update readme regarding rebrand 2023-06-15 10:26:15 +02:00
Ayo Ayco
8011bbd930
fix: address bar nav buttons are enabled on home (#54)
* fix: disable the nav buttons on home

* style: subtler color of address bar text

* 0.1.24
2023-06-15 10:04:10 +02:00
Ayo
f913b1f3d2 fix: page title missing space 2023-06-15 08:52:00 +02:00
Ayo
9e2818004a fix: history heading shows when only index is cached 2023-06-15 08:43:41 +02:00
Ayo Ayco
7c949d6030
feat: preserve home scroll position (#52)
* feat: preserve home scroll position

* 0.1.23
2023-06-14 23:31:06 +02:00
Ayo Ayco
99a028a64f
fix: index not cached (#48)
* fix: make sure to cache index

* 0.1.22
2023-06-14 22:55:00 +02:00
Ayo Ayco
b2541606e4
feat: preserve history (#47)
* feat: preserve history

* 0.1.21
2023-06-14 22:38:06 +02:00
Ayo
d2f698571e style: footer spacing 2023-06-14 21:20:14 +02:00
Ayo Ayco
968cc03cd4
feat: update disclaimer (#46)
* feat: update disclaimer

* 0.1.20

* 0.1.21

* 0.1.19

* 0.1.20
2023-06-14 21:15:02 +02:00
Ayo
e5026bfeaa 0.1.19 2023-06-14 20:49:58 +02:00
Ayo Ayco
3a49d5d0b5
feat: add footer description, disclaimers, attribution, links (#43) 2023-06-14 20:49:33 +02:00
Ayo Ayco
c8cc39958c
feat: hide dynamic buttons if js not allowed (#42)
* feat: hide dynamic buttons by default & show if js allowed

* 0.1.18
2023-06-14 19:59:20 +02:00
Ayo
16a00d7c9d style: adjust disabled button color 2023-06-14 19:48:41 +02:00
Ayo Ayco
b76f00ac35
feat: address bar button states (#41)
* feat: address bar button states

* 0.1.17
2023-06-14 19:45:57 +02:00
Ayo
5e8d2cf495 fix: typo 2023-06-14 18:18:06 +02:00
Ayo Ayco
68aa6b1502
a11y: add accessible button names (#40)
* a11y: add accessible button names

* 0.1.16
2023-06-14 18:12:53 +02:00
Ayo Ayco
a03e58eeb6
chore: update README.md 2023-06-14 17:25:33 +02:00
Ayo
4ff7ab0a4f chore: add screenshot asset 2023-06-14 17:17:10 +02:00
70 changed files with 11102 additions and 6426 deletions

11
.build.yml Normal file
View file

@ -0,0 +1,11 @@
image: alpine/edge
secrets:
- bbfcb6dc-7c4a-42ee-a11a-022f0339a133
environment:
REPO: cozy
GH_USER: ayoayco
tasks:
- push-mirror: |
cd ~/"${REPO}"
git config --global credential.helper store
git push --mirror "https://github.com/${GH_USER}/${REPO}"

BIN
.github/assets/screen-2.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
.github/assets/screenshot.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

5
.github/pull_request_template.md vendored Normal file
View file

@ -0,0 +1,5 @@
# Your Pull Request Might Not Be Merged
This is a mirror of the [Sourcehut](https://git.sr.ht/~ayoayco/cozy) repository.
If you want to contribute, please do so on Sourcehut. [Here is how](https://git-send-email.io).

6
.gitignore vendored
View file

@ -1,7 +1,11 @@
node_modules/
dist/
.netlify/
.astro/
.output/
.continue/
package-lock.json
*~
*swo
*swp
.eslintcache

1
.husky/pre-commit Executable file
View file

@ -0,0 +1 @@
npx lint-staged

7
.prettierignore Normal file
View file

@ -0,0 +1,7 @@
# someday let's think about formatting html
**/*.html
**/*.md
**/*.css
**/*.yml
**/*.yaml

1
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1 @@
{}

View file

@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
- Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
- The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
@ -59,8 +59,7 @@ representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
[INSERT CONTACT METHOD].
reported via email to the community leaders responsible for enforcement at ayo.ayco@pm.me
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the

View file

@ -4,10 +4,16 @@ Thank you for your interest in building the coziest web experience for people li
## Tech Stack
You will need [to install nodejs](https://nodejs.org/en/download) and [pnpm](https://pnpm.io/installation) if you do not have them yet in your machine.
This project is built with the following tech
1. TypeScript, HTML, CSS - even just knowing basic JS and CSS will give you familiarity of the code
1. [Astro](https://astro.build) - Astro is the chosen framework, please read throught the basics on their docs if you are unfamiliar
1. [Vite](https://vitejs.dev) - Vite is the bundler Astro is using and it allows us to do server-side rendering
1. [@extractus/article-extractor](https://www.npmjs.com/package/@extractus/article-extractor) - Article Extractor is the library we use to fetch and extract article content
- we don't use any framework that ships to the browser, we only write Astro components for server-side rendering, and vanilla DOM manipulation via `script` tags.
1. [@extractus/article-extractor](https://www.npmjs.com/package/@extractus/article-extractor) - Article Extractor is the library we use to fetch and extract article content
## Setting up the project
@ -17,17 +23,17 @@ This project is built with the following tech
git clone https://github.com/ayoayco/cozy.git
```
2. install dependencies
2. install dependencies using `pnpm`
```
cd cozy
npm install
pnpm install
```
3. run the dev server
```
npm start
pnpm start
```
## Good first issues

51
Dockerfile Normal file
View file

@ -0,0 +1,51 @@
FROM docker.io/library/node:lts-alpine AS base
# Prepare work directory
WORKDIR /cozy
FROM base AS builder
# Prepare pnpm https://pnpm.io/installation#using-corepack
# workaround for npm registry key change
# ref. `pnpm@10.1.0` / `pnpm@9.15.4` cannot be installed due to key id mismatch · Issue #612 · nodejs/corepack
# - https://github.com/nodejs/corepack/issues/612#issuecomment-2629496091
RUN npm i -g corepack@latest && corepack enable
# Prepare deps
RUN apk update
RUN apk add git --no-cache
# Copy all source files
COPY package.json ./
COPY pnpm-lock.yaml ./
# Run full install
RUN pnpm i --frozen-lockfile --ignore-scripts
# Copy source
COPY . ./
# Build
RUN pnpm build
FROM base AS runner
ARG UID=911
ARG GID=911
# Create a dedicated user and group
RUN set -eux; \
addgroup -g $GID cozy; \
adduser -u $UID -D -G cozy cozy;
USER cozy
ENV NODE_ENV=production
COPY --from=builder /cozy/ ./
EXPOSE 4321/tcp
ENV PORT=4321
CMD ["node", "server.mjs"]

View file

@ -1,51 +1,97 @@
<h1 align="center">Cozy 🧸</h1>
<p align="center">
<a href="https://github.com/ayoayco/cozy">
<img alt="Last Commit" src="https://img.shields.io/github/last-commit/ayoayco/cozy?logo=github" />
</a>
<a href="https://github.com/ayoayco/cozy-reader/releases/latest"><img alt="Alpha Version" src="https://img.shields.io/github/package-json/v/ayoayco/cozy?label=alpha" /></a><br />
Remove distractions. Save for later. Get useful insights.<br />
Cozy is your modern-day reading assistant.
<img src="https://github.com/user-attachments/assets/e49b56a7-cc0f-45a3-98e0-8bbcbd02a47c" alt="COZY logo" /><br />
Remove distractions. Save for later.<br />
Cozy is your modern-day reading companion.
</p>
<p align="center">
<a href="https://github.com/ayoayco/cozy">
<img alt="Last Commit" src="https://img.shields.io/github/last-commit/ayoayco/cozy?logo=github" />
</a>
<a href="https://github.com/ayoayco/cozy-reader/releases/latest">
<img alt="Alpha" src="https://img.shields.io/github/package-json/v/ayoayco/cozy?label=alpha" />
</a><br />
</p>
## Why?
Visiting websites in this 'modern' time is a paradox: standard Web technologies are better but most commercial websites are pretty terrible—not only because misinformation abounds online, but also because of Big Tech's personal data farming that puts us all at a disadvantage.
[**Cozy**](https://cozy.pub) addresses this by putting people first by default: no tracking cookies will ever get into your browser, pay the minimum bandwidth to get information you need, and save everything on your browser for accessing them again later offline.
With Cozy: _The Web is Yours._
## How is this app different?
Here's what this project is building:
1. An app that just works, no sign-ups or set-ups.
2. Progressively enhanced experience. Main feature works even without JS. Removing distractions happen on the server and dead clean HTML gets delivered
3. All your data are cached and does not leave your device; offline access is by default
4. Cloud-sync will be opt-in, with your choice of provider
5. Will also explore smart insights, such as:
1. text summarization
2. dominant emotion
3. other sources for lateral reading
## Report bugs or contribute
Get in touch:
1. Chat via Discord: [Ayo's Projects](https://discord.gg/kkvW7GYNAp)
1. Submit tickets via [SourceHut todo](https://todo.sr.ht/~ayoayco/astro-sw)
1. Start a [GitHub discussion](https://github.com/ayoayco/astro-sw/discussions)
1. Email me: [ayo@ayco.io](mailto:ayo@ayco.io)
## Roadmap
| Feature | Status |
| --- | --- |
| Remove distractions| ✅ DONE |
| Save viewed history | ✅ DONE |
| Set items as Favorites | 🛠️ In-progress |
| PWA: full Offline access | |
| AI insights | |
| Browser Extensions | |
| Native Apps | |
| Feature | Status | Version |
| ---------------------- | -------------- | ------- |
| Remove distractions | ✅ DONE | v0.0.x |
| Save viewed history | ✅ DONE | v0.1.x |
| Open links within Cozy | ✅ DONE | v0.2.x |
| Offline access | ✅ DONE | v0.3.x |
| Set items as Favorites | 🛠️ In-progress | v0.4.x |
| Smart insights | | v0.5.x |
| Browser Extensions | | |
| Native Apps | | |
## Usage / Options
**1. Copy, Paste**
It's simple. When you open an article and want to turn it into a more cozy reading experience, just copy the url and paste it to the [Cozy](https://cozy-reader.netlify.app/) address bar.
It's simple. When you open an article and want to turn it into a more cozy reading experience, just copy the url and paste it to the app's address bar.
**2. One-click Bookmark**
A bookmarklet could run a script to open the current page for you on Cozy. You can create this new bookmark titled 'Get cozy!' and put the following as value for the URL:
A bookmarklet could run a script to open the current page for you on ~~Cozy~~. You can create this new bookmark titled 'Get cozy!' and put the following as value for the URL:
```
javascript:(function(){ window.open('https://cozy-reader.netlify.app/?url=%27 + window.location.href, %27_self%27); })();
javascript:(function(){ window.open('https://cozy.pub/?url=%27 + window.location.href, %27_self%27); })();
```
This is possible on all major browsers, including Safari on iOS (where I personally use this often). Some screenshots:
| Firefox | Chrome |
| --- | --- |
| Firefox | Chrome |
| --------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| ![Screenshot from 2023-05-13 08-31-41](https://github.com/ayoayco/cozy/assets/4262489/9b296d4f-2722-483a-bbc2-431c6b2ae996) | ![Screenshot from 2023-05-12 23-32-08](https://github.com/ayoayco/cozy/assets/4262489/144b74f8-3949-46b9-849c-351e4af0ac12) |
**3. One-click Extension then...**
It doesn't exist yet... I'll probably get to creating a browser extension at some point. But for now, PRs welcome! :)
## Credits
We are thankful for all the building blocks provided by the following projects:
1. [Astro](https://www.npmjs.com/package/astro) for our server-side rendering framework
1. [@ayco/astro-sw](https://ayco.io/n/@ayco/astro-sw) for taking app's service worker, and injecting needed dynamic assets & variables
1. [@ayco/astro-resume](https://ayco.io/n/@ayco/astro-resume) for serializing server data to be used in the client
1. [@extractus/article-extractor](https://www.npmjs.com/package/@extractus/article-extractor) for the amazing scraping of articles
1. [astro-iconify](https://www.npmjs.com/package/astro-iconify) for easily using icon-sets in [iconify](https://icon-sets.iconify.design/)
1. [ultrahtml](https://www.npmjs.com/package/ultrahtml) for any cleanup and transformation we do on the received article content
1. [fastify](https://fastify.dev/) for our production server and [nginx](https://nginx.org/) as reverse proxy
## Contributing
Speaking of PRs being welcome, see our [CONTRIBUTING guide](/CONTRIBUTING.md).
🧸
If any of the above seems to need improvement for you, we are always eager to hear feedback and welcome all contributions. See our [CONTRIBUTING](/CONTRIBUTING.md) guide for more info.
Join our [Discord](https://discord.gg/kkvW7GYNAp) if you need help!

View file

@ -1,8 +1,34 @@
import { defineConfig } from "astro/config";
import netlify from "@astrojs/netlify/functions";
// @ts-check
import { defineConfig } from 'astro/config'
import node from '@astrojs/node'
import sitemap from '@astrojs/sitemap'
import serviceWorker from '@ayco/astro-sw'
import { VERSION } from './src/consts'
// https://astro.build/config
export default defineConfig({
output: "server",
adapter: netlify()
});
output: 'static',
site: 'https://cozy.pub/',
adapter: node({
mode: 'middleware',
}),
vite: {
server: {
fs: {
strict: false,
},
},
},
integrations: [
sitemap(),
serviceWorker({
path: './src/sw.mjs',
assetCachePrefix: 'cozy-reader',
assetCacheVersionID: VERSION,
logAssets: true,
esbuild: {
minify: true,
},
}),
],
})

BIN
branding/Cozy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="487.1" height="151.38" version="1.1" viewBox="0 0 128.88 40.052" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-1.8084 -44.644)">
<path d="m27.977 63.101q2.0212 0 3.2445 2.1808 1.2765 2.1276 1.2765 5.7444 0 6.2763-3.7764 9.7868-3.7233 3.4573-10.425 3.4573-7.9252 0-12.234-4.9998-4.2551-4.9998-4.2551-14.361 0-4.6807 1.0638-8.4039 1.117-3.7233 3.1382-6.3295 2.0744-2.6595 4.9998-4.0424 2.9254-1.4361 6.5955-1.4361 2.6595 0 4.787 0.74465 2.1808 0.74465 3.7233 2.1276 1.5425 1.3297 2.3403 3.2445 0.85103 1.8616 0.85103 4.202 0 1.6489-0.42551 3.0318-0.37232 1.3829-1.0638 2.3935-0.69146 1.0106-1.7021 1.5957-0.95741 0.53189-2.0744 0.53189-1.7552 0-2.8722-1.2765-1.117-1.3297-1.117-3.3509 0-0.90422-0.26595-1.7552-0.21276-0.85103-0.58508-1.4893t-0.90422-1.0106q-0.4787-0.37232-1.0638-0.37232-0.69146 0-1.3297 0.63827-0.63827 0.58508-1.117 1.7021-0.4787 1.0638-0.79784 2.4999-0.26595 1.3829-0.26595 2.9786 0 3.1382 1.0638 4.8402 1.117 1.7021 3.1382 1.7021 1.3829 0 2.6595-0.69146 1.2765-0.74465 2.4999-1.5957 1.2234-0.85103 2.4467-1.5425 1.2234-0.74465 2.4467-0.74465zm19.786 0.58508q0 0.95741 0.21276 2.0212 0.21276 1.0106 0.63827 1.9148 0.42552 0.85103 1.117 1.4361 0.69146 0.58508 1.7021 0.58508 1.0638 0 1.6489-0.53189 0.63827-0.53189 0.95741-1.3829 0.31914-0.90422 0.42552-1.968 0.10638-1.117 0.10638-2.234t-0.26595-2.3403q-0.21276-1.2234-0.69146-2.234-0.42552-1.0638-1.117-1.7552-0.63827-0.69146-1.4893-0.69146-0.58508 0-1.1702 0.53189-0.58508 0.53189-1.0638 1.4893-0.42551 0.90422-0.74465 2.234-0.26595 1.3297-0.26595 2.9254zm2.9254 21.01q-3.9892 0-7.0742-1.117-3.085-1.117-5.2126-3.5105-2.0744-2.3935-3.1382-6.17-1.0638-3.7764-1.0638-9.0954 0-5.1062 1.3297-8.723 1.3829-3.6701 3.7233-6.0104 2.3935-2.3935 5.5849-3.4573 3.1914-1.117 6.9146-1.117 3.7764 0 6.8614 1.1702 3.1382 1.1702 5.3189 3.4573 2.234 2.2871 3.4573 5.7444 1.2234 3.4041 1.2234 7.9784 0 10.372-4.6275 15.638-4.5743 5.2126-13.297 5.2126zm46.913-32.02q0 1.2765-0.69146 2.9786-0.69146 1.7021-1.7552 3.5637-1.0106 1.8084-2.234 3.6169-1.1702 1.7552-2.234 3.1914-1.0106 1.4361-1.7021 2.3403t-0.69146 1.0106q0 0.42551 0.4787 0.4787 0.53189 0 0.79784 0 0.79784 0 1.3829 0 0.58508-0.05319 1.0638-0.05319 0.53189-0.05319 1.117-0.05319 0.58508-0.05319 1.3829-0.05319 1.9148 0 3.1382 0.42551 1.2765 0.37232 1.968 1.3297 0.74466 0.90422 1.0106 2.4467 0.26595 1.4893 0.26595 3.6701 0 2.4467-0.15957 3.7764-0.15957 1.3297-1.3297 1.9148-1.1702 0.58508-3.8296 0.69146-2.6595 0.10638-7.6061 0.10638-1.7021 0-3.4573-0.05319-1.7552-0.05319-3.4573-0.05319-2.0212 0-3.9892-0.21276-1.968-0.15957-3.5637-0.85103-1.5425-0.74465-2.5531-2.1808-0.95741-1.4361-0.95741-3.936 0-1.7021 1.0106-3.6701 1.0106-2.0212 2.4467-4.0424 1.4361-2.0212 2.9786-3.8828 1.5957-1.8616 2.7127-3.1914 0.26595-0.31914 0.79784-0.85103t1.0638-1.0638 0.90422-1.0106q0.37232-0.4787 0.31914-0.74465-0.05319-0.42552-0.26595-0.42552-0.15957-0.05319-0.26595-0.05319-1.6489 0-3.2977 0.26595-1.5957 0.26595-3.2445 0.26595-1.2234 0-2.0744-0.63827-0.85103-0.63827-1.3829-1.6489-0.4787-1.0638-0.74465-2.3935-0.21276-1.3297-0.21276-2.6595 0-1.968 1.3829-3.1914 1.4361-1.2234 3.4041-1.9148 2.0212-0.69146 4.202-0.90422 2.1808-0.26595 3.6701-0.26595 2.3403 0 4.8402 0.05319 2.4999 0.05319 4.5743 0.79784 2.0744 0.69146 3.4041 2.3403 1.3829 1.5957 1.3829 4.7338zm2.0742-0.58508q0-1.5425 0.26595-2.819 0.26594-1.2765 0.90422-2.1808 0.63827-0.90422 1.7552-1.3829 1.117-0.53189 2.819-0.53189 2.6595 0 3.936 0.69146 1.2766 0.63827 1.8084 2.4999 0.26595 0.85103 0.42551 1.4893 0.15957 0.58508 0.37233 1.0638 0.26595 0.4787 0.63827 0.95741 0.42552 0.42552 1.117 0.95741 0.26594 0.21276 0.58508 0.37232 0.31913 0.15957 0.69146 0.15957 2.1276 0 3.3509-1.1702 1.2765-1.1702 1.5425-3.1382 0.15957-1.3297 0.69147-2.1808 0.58508-0.85103 1.3829-1.3297 0.79784-0.53189 1.7021-0.69146 0.90421-0.21276 1.8084-0.21276 2.3403 0 3.7764 1.3829t1.4361 3.7764q0 2.6063-0.37232 4.6275-0.31914 2.0212-1.117 3.7764-0.74465 1.7021-2.0212 3.2977-1.2234 1.5957-2.9786 3.3509-1.7552 1.8084-2.2871 3.085-0.53189 1.2234-0.53189 1.7552 0 0.95741 0.10637 1.8084 0.15957 0.85103 0.26595 1.7021 0.15957 0.85103 0.26595 1.7552 0.15956 0.90422 0.15956 2.0212 0 1.4893-0.42551 2.819-0.37232 1.2765-1.2234 2.2871t-2.234 1.5957q-1.3829 0.58508-3.3509 0.58508-3.9892 0-5.7976-1.8084-1.8084-1.8084-1.8084-5.6913 0-1.0106 0.10638-1.7552 0.15957-0.79784 0.31913-1.4893 0.21276-0.74465 0.31914-1.5425 0.15957-0.79784 0.15957-1.8084 0-1.6489-0.74465-2.9254t-2.1276-2.6595q-2.5531-2.3935-4.1488-5.5849-1.5425-3.2445-1.5425-6.9146z" stroke-width=".27703" aria-label="COZY"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

BIN
branding/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

53
branding/logo.svg Normal file
View file

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="500"
height="500"
viewBox="0 0 132.29166 132.29167"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"
sodipodi:docname="logo.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="1.0280428"
inkscape:cx="187.24901"
inkscape:cy="249.98959"
inkscape:window-width="1452"
inkscape:window-height="752"
inkscape:window-x="1567"
inkscape:window-y="188"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<circle
style="fill:#ffffff;stroke-width:0.275733"
id="path1"
cx="66.32341"
cy="66.615196"
r="65.904121" />
<path
d="m 94.18155,62.84604 q 5.319924,0 8.53988,5.739918 3.35995,5.59992 3.35995,15.119784 0,16.519768 -9.939858,25.759628 -9.79986,9.09987 -27.439608,9.09987 -20.859703,0 -32.199541,-13.15981 -11.19984,-13.15981 -11.19984,-37.799458 0,-12.319824 2.79996,-22.119684 2.939958,-9.79986 8.259882,-16.659762 5.459922,-6.9999 13.159812,-10.639848 7.69989,-3.779946 17.359753,-3.779946 6.9999,0 12.59982,1.959972 5.739918,1.959972 9.79986,5.59992 4.059942,3.49995 6.159912,8.539878 2.239968,4.89993 2.239968,11.059842 0,4.339938 -1.119984,7.979886 -0.979986,3.639948 -2.79996,6.29991 -1.819974,2.659962 -4.479936,4.19994 -2.519964,1.39998 -5.459922,1.39998 -4.619934,0 -7.559892,-3.359952 -2.939958,-3.49995 -2.939958,-8.819874 0,-2.379966 -0.69999,-4.619934 -0.559992,-2.239968 -1.539978,-3.919944 -0.979986,-1.679976 -2.379966,-2.659962 -1.259982,-0.979986 -2.79996,-0.979986 -1.819974,0 -3.49995,1.679976 -1.679977,1.539978 -2.939959,4.479936 -1.259982,2.79996 -2.09997,6.579906 -0.69999,3.639948 -0.69999,7.839888 0,8.259882 2.79996,12.739818 2.939959,4.479936 8.259883,4.479936 3.639948,0 6.9999,-1.819974 3.359952,-1.959972 6.579906,-4.19994 3.219954,-2.239968 6.439908,-4.059942 3.219954,-1.959972 6.439908,-1.959972 z"
id="text1"
style="font-size:139.998px;font-family:'Super Frog';-inkscape-font-specification:'Super Frog';stroke-width:0.729151"
aria-label="C" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

50
branding/text-logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7 KiB

7
docker-compose.yaml Normal file
View file

@ -0,0 +1,7 @@
services:
cozy:
build:
context: .
dockerfile: Dockerfile
ports:
- 4321:4321

53
eslint.config.mjs Normal file
View file

@ -0,0 +1,53 @@
import globals from 'globals'
import eslintPluginAstro from 'eslint-plugin-astro'
import jsPlugin from '@eslint/js'
import tseslint from 'typescript-eslint'
import astroSwGlobals from '@ayco/astro-sw/globals'
import astroParser from 'astro-eslint-parser'
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
import { includeIgnoreFile } from '@eslint/compat'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const gitignorePath = path.resolve(__dirname, '.gitignore')
export default [
{
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
},
},
// add more generic rule sets here, such as:
jsPlugin.configs.recommended,
eslintPluginPrettierRecommended,
...tseslint.configs.recommended,
...eslintPluginAstro.configs['recommended'],
...eslintPluginAstro.configs['jsx-a11y-recommended'],
includeIgnoreFile(gitignorePath),
{
ignores: ['**/env.d.ts'],
},
{
files: ['**/*.astro'],
languageOptions: {
parser: astroParser,
parserOptions: {
parser: tseslint.parser,
},
},
},
{
files: ['**/sw.mjs'],
languageOptions: {
globals: {
...astroSwGlobals,
},
},
},
]

15
example.service Normal file
View file

@ -0,0 +1,15 @@
[Unit]
Description=Cozy
[Service]
ExecStart=/home/ayo/cozy/server.mjs
Restart=always
User=nobody
# Note Debian/Ubuntu uses 'nogroup', RHEL/Fedora uses 'nobody'
Group=nogroup
Environment=PATH=/usr/bin:/usr/local/bin
Environment=NODE_ENV=production
WorkingDirectory=/home/ayo/cozy
[Install]
WantedBy=multi-user.target

6
logo-cropped-min.svg Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="487.1" height="151.38" version="1.1" viewBox="0 0 128.88 40.052" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-1.8084 -44.644)">
<path d="m27.977 63.101q2.0212 0 3.2445 2.1808 1.2765 2.1276 1.2765 5.7444 0 6.2763-3.7764 9.7868-3.7233 3.4573-10.425 3.4573-7.9252 0-12.234-4.9998-4.2551-4.9998-4.2551-14.361 0-4.6807 1.0638-8.4039 1.117-3.7233 3.1382-6.3295 2.0744-2.6595 4.9998-4.0424 2.9254-1.4361 6.5955-1.4361 2.6595 0 4.787 0.74465 2.1808 0.74465 3.7233 2.1276 1.5425 1.3297 2.3403 3.2445 0.85103 1.8616 0.85103 4.202 0 1.6489-0.42551 3.0318-0.37232 1.3829-1.0638 2.3935-0.69146 1.0106-1.7021 1.5957-0.95741 0.53189-2.0744 0.53189-1.7552 0-2.8722-1.2765-1.117-1.3297-1.117-3.3509 0-0.90422-0.26595-1.7552-0.21276-0.85103-0.58508-1.4893t-0.90422-1.0106q-0.4787-0.37232-1.0638-0.37232-0.69146 0-1.3297 0.63827-0.63827 0.58508-1.117 1.7021-0.4787 1.0638-0.79784 2.4999-0.26595 1.3829-0.26595 2.9786 0 3.1382 1.0638 4.8402 1.117 1.7021 3.1382 1.7021 1.3829 0 2.6595-0.69146 1.2765-0.74465 2.4999-1.5957 1.2234-0.85103 2.4467-1.5425 1.2234-0.74465 2.4467-0.74465zm19.786 0.58508q0 0.95741 0.21276 2.0212 0.21276 1.0106 0.63827 1.9148 0.42552 0.85103 1.117 1.4361 0.69146 0.58508 1.7021 0.58508 1.0638 0 1.6489-0.53189 0.63827-0.53189 0.95741-1.3829 0.31914-0.90422 0.42552-1.968 0.10638-1.117 0.10638-2.234t-0.26595-2.3403q-0.21276-1.2234-0.69146-2.234-0.42552-1.0638-1.117-1.7552-0.63827-0.69146-1.4893-0.69146-0.58508 0-1.1702 0.53189-0.58508 0.53189-1.0638 1.4893-0.42551 0.90422-0.74465 2.234-0.26595 1.3297-0.26595 2.9254zm2.9254 21.01q-3.9892 0-7.0742-1.117-3.085-1.117-5.2126-3.5105-2.0744-2.3935-3.1382-6.17-1.0638-3.7764-1.0638-9.0954 0-5.1062 1.3297-8.723 1.3829-3.6701 3.7233-6.0104 2.3935-2.3935 5.5849-3.4573 3.1914-1.117 6.9146-1.117 3.7764 0 6.8614 1.1702 3.1382 1.1702 5.3189 3.4573 2.234 2.2871 3.4573 5.7444 1.2234 3.4041 1.2234 7.9784 0 10.372-4.6275 15.638-4.5743 5.2126-13.297 5.2126zm46.913-32.02q0 1.2765-0.69146 2.9786-0.69146 1.7021-1.7552 3.5637-1.0106 1.8084-2.234 3.6169-1.1702 1.7552-2.234 3.1914-1.0106 1.4361-1.7021 2.3403t-0.69146 1.0106q0 0.42551 0.4787 0.4787 0.53189 0 0.79784 0 0.79784 0 1.3829 0 0.58508-0.05319 1.0638-0.05319 0.53189-0.05319 1.117-0.05319 0.58508-0.05319 1.3829-0.05319 1.9148 0 3.1382 0.42551 1.2765 0.37232 1.968 1.3297 0.74466 0.90422 1.0106 2.4467 0.26595 1.4893 0.26595 3.6701 0 2.4467-0.15957 3.7764-0.15957 1.3297-1.3297 1.9148-1.1702 0.58508-3.8296 0.69146-2.6595 0.10638-7.6061 0.10638-1.7021 0-3.4573-0.05319-1.7552-0.05319-3.4573-0.05319-2.0212 0-3.9892-0.21276-1.968-0.15957-3.5637-0.85103-1.5425-0.74465-2.5531-2.1808-0.95741-1.4361-0.95741-3.936 0-1.7021 1.0106-3.6701 1.0106-2.0212 2.4467-4.0424 1.4361-2.0212 2.9786-3.8828 1.5957-1.8616 2.7127-3.1914 0.26595-0.31914 0.79784-0.85103t1.0638-1.0638 0.90422-1.0106q0.37232-0.4787 0.31914-0.74465-0.05319-0.42552-0.26595-0.42552-0.15957-0.05319-0.26595-0.05319-1.6489 0-3.2977 0.26595-1.5957 0.26595-3.2445 0.26595-1.2234 0-2.0744-0.63827-0.85103-0.63827-1.3829-1.6489-0.4787-1.0638-0.74465-2.3935-0.21276-1.3297-0.21276-2.6595 0-1.968 1.3829-3.1914 1.4361-1.2234 3.4041-1.9148 2.0212-0.69146 4.202-0.90422 2.1808-0.26595 3.6701-0.26595 2.3403 0 4.8402 0.05319 2.4999 0.05319 4.5743 0.79784 2.0744 0.69146 3.4041 2.3403 1.3829 1.5957 1.3829 4.7338zm2.0742-0.58508q0-1.5425 0.26595-2.819 0.26594-1.2765 0.90422-2.1808 0.63827-0.90422 1.7552-1.3829 1.117-0.53189 2.819-0.53189 2.6595 0 3.936 0.69146 1.2766 0.63827 1.8084 2.4999 0.26595 0.85103 0.42551 1.4893 0.15957 0.58508 0.37233 1.0638 0.26595 0.4787 0.63827 0.95741 0.42552 0.42552 1.117 0.95741 0.26594 0.21276 0.58508 0.37232 0.31913 0.15957 0.69146 0.15957 2.1276 0 3.3509-1.1702 1.2765-1.1702 1.5425-3.1382 0.15957-1.3297 0.69147-2.1808 0.58508-0.85103 1.3829-1.3297 0.79784-0.53189 1.7021-0.69146 0.90421-0.21276 1.8084-0.21276 2.3403 0 3.7764 1.3829t1.4361 3.7764q0 2.6063-0.37232 4.6275-0.31914 2.0212-1.117 3.7764-0.74465 1.7021-2.0212 3.2977-1.2234 1.5957-2.9786 3.3509-1.7552 1.8084-2.2871 3.085-0.53189 1.2234-0.53189 1.7552 0 0.95741 0.10637 1.8084 0.15957 0.85103 0.26595 1.7021 0.15957 0.85103 0.26595 1.7552 0.15956 0.90422 0.15956 2.0212 0 1.4893-0.42551 2.819-0.37232 1.2765-1.2234 2.2871t-2.234 1.5957q-1.3829 0.58508-3.3509 0.58508-3.9892 0-5.7976-1.8084-1.8084-1.8084-1.8084-5.6913 0-1.0106 0.10638-1.7552 0.15957-0.79784 0.31913-1.4893 0.21276-0.74465 0.31914-1.5425 0.15957-0.79784 0.15957-1.8084 0-1.6489-0.74465-2.9254t-2.1276-2.6595q-2.5531-2.3935-4.1488-5.5849-1.5425-3.2445-1.5425-6.9146z" stroke-width=".27703" aria-label="COZY"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

6030
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,16 +1,73 @@
{
"name": "@ayco/cozy",
"version": "0.1.15",
"version": "0.3.3",
"repository": {
"type": "git",
"url": "https://github.com/ayoayco/cozy"
},
"engines": {
"node": ">=18.0.0"
},
"homepage": "https://cozy.pub",
"scripts": {
"start": "astro dev",
"build": "astro build"
},
"devDependencies": {
"astro": "^2.5.7"
"dev": "astro dev",
"build": "astro telemetry disable && astro build",
"build:server": "esbuild server.mjs --bundle --platform=node --packages=external --format=esm --outfile=server-bundle.mjs",
"preview": "node ./server.mjs",
"build:preview": "npm run build && npm run preview",
"publish:patch": "npm version patch && npm publish --access public",
"publish:minor": "npm version minor && npm publish --access public",
"deploy:client": "npm run build && scp -r dist/client/ ayo@ayco.io:~/cozy/dist/",
"test": "vitest",
"prepare": "husky && husky install",
"lint": "eslint . --config eslint.config.mjs --cache",
"format": "prettier . --write"
},
"dependencies": {
"@astrojs/netlify": "^2.2.2",
"@extractus/article-extractor": "^7.2.15",
"sass": "^1.62.1"
"@astrojs/node": "^9.3.3",
"@astrojs/rss": "^4.0.12",
"@astrojs/sitemap": "^3.4.2",
"@ayco/astro-resume": "^0.4.4",
"@ayco/astro-sw": "^0.8.14",
"@fastify/middie": "^9.0.3",
"@fastify/rate-limit": "^10.3.0",
"@fastify/static": "^8.2.0",
"astro": "^5.12.8",
"astro-iconify": "^1.2.0",
"fastify": "^5.4.0",
"redis": "^5.8.0",
"ultrahtml": "^1.6.0"
},
"devDependencies": {
"@astrojs/check": "^0.9.4",
"@eslint/compat": "^1.3.1",
"@eslint/js": "^9.32.0",
"@extractus/article-extractor": "^8.0.19",
"astro-eslint-parser": "^1.2.2",
"esbuild": "^0.25.8",
"eslint": "^9.32.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-astro": "^1.3.1",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.5.4",
"globals": "^16.3.0",
"husky": "^9.1.7",
"lint-staged": "^16.1.4",
"prettier": "^3.6.2",
"prettier-plugin-astro": "^0.14.1",
"sass": "^1.90.0",
"typescript": "^5.9.2",
"typescript-eslint": "^8.39.0",
"vitest": "^3.2.4"
},
"lint-staged": {
"*.{js,mjs,astro,ts}": [
"prettier --write",
"eslint --fix"
],
"*.json": [
"prettier --write"
]
}
}

8424
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load diff

21
prettier.config.mjs Normal file
View file

@ -0,0 +1,21 @@
/**
* @see https://prettier.io/docs/en/configuration.html
* @type {import("prettier").Config}
*/
const config = {
trailingComma: 'es5',
tabWidth: 2,
semi: false,
singleQuote: true,
plugins: ['prettier-plugin-astro'],
overrides: [
{
files: '*.astro',
options: {
parser: 'astro',
},
},
],
}
export default config

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/ayoayco-avatar.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

BIN
public/cozy.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

53
public/favicon.svg Normal file
View file

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="500"
height="500"
viewBox="0 0 132.29166 132.29167"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"
sodipodi:docname="logo.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="1.0280428"
inkscape:cx="187.24901"
inkscape:cy="249.98959"
inkscape:window-width="1452"
inkscape:window-height="752"
inkscape:window-x="1567"
inkscape:window-y="188"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<circle
style="fill:#ffffff;stroke-width:0.275733"
id="path1"
cx="66.32341"
cy="66.615196"
r="65.904121" />
<path
d="m 94.18155,62.84604 q 5.319924,0 8.53988,5.739918 3.35995,5.59992 3.35995,15.119784 0,16.519768 -9.939858,25.759628 -9.79986,9.09987 -27.439608,9.09987 -20.859703,0 -32.199541,-13.15981 -11.19984,-13.15981 -11.19984,-37.799458 0,-12.319824 2.79996,-22.119684 2.939958,-9.79986 8.259882,-16.659762 5.459922,-6.9999 13.159812,-10.639848 7.69989,-3.779946 17.359753,-3.779946 6.9999,0 12.59982,1.959972 5.739918,1.959972 9.79986,5.59992 4.059942,3.49995 6.159912,8.539878 2.239968,4.89993 2.239968,11.059842 0,4.339938 -1.119984,7.979886 -0.979986,3.639948 -2.79996,6.29991 -1.819974,2.659962 -4.479936,4.19994 -2.519964,1.39998 -5.459922,1.39998 -4.619934,0 -7.559892,-3.359952 -2.939958,-3.49995 -2.939958,-8.819874 0,-2.379966 -0.69999,-4.619934 -0.559992,-2.239968 -1.539978,-3.919944 -0.979986,-1.679976 -2.379966,-2.659962 -1.259982,-0.979986 -2.79996,-0.979986 -1.819974,0 -3.49995,1.679976 -1.679977,1.539978 -2.939959,4.479936 -1.259982,2.79996 -2.09997,6.579906 -0.69999,3.639948 -0.69999,7.839888 0,8.259882 2.79996,12.739818 2.939959,4.479936 8.259883,4.479936 3.639948,0 6.9999,-1.819974 3.359952,-1.959972 6.579906,-4.19994 3.219954,-2.239968 6.439908,-4.059942 3.219954,-1.959972 6.439908,-1.959972 z"
id="text1"
style="font-size:139.998px;font-family:'Super Frog';-inkscape-font-specification:'Super Frog';stroke-width:0.729151"
aria-label="C" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Binary file not shown.

9
public/jumbotron.svg Normal file
View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="487.1" height="151.38" version="1.1" viewBox="0 0 128.88 40.052"
xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-1.8084 -44.644)">
<path
d="m27.977 63.101q2.0212 0 3.2445 2.1808 1.2765 2.1276 1.2765 5.7444 0 6.2763-3.7764 9.7868-3.7233 3.4573-10.425 3.4573-7.9252 0-12.234-4.9998-4.2551-4.9998-4.2551-14.361 0-4.6807 1.0638-8.4039 1.117-3.7233 3.1382-6.3295 2.0744-2.6595 4.9998-4.0424 2.9254-1.4361 6.5955-1.4361 2.6595 0 4.787 0.74465 2.1808 0.74465 3.7233 2.1276 1.5425 1.3297 2.3403 3.2445 0.85103 1.8616 0.85103 4.202 0 1.6489-0.42551 3.0318-0.37232 1.3829-1.0638 2.3935-0.69146 1.0106-1.7021 1.5957-0.95741 0.53189-2.0744 0.53189-1.7552 0-2.8722-1.2765-1.117-1.3297-1.117-3.3509 0-0.90422-0.26595-1.7552-0.21276-0.85103-0.58508-1.4893t-0.90422-1.0106q-0.4787-0.37232-1.0638-0.37232-0.69146 0-1.3297 0.63827-0.63827 0.58508-1.117 1.7021-0.4787 1.0638-0.79784 2.4999-0.26595 1.3829-0.26595 2.9786 0 3.1382 1.0638 4.8402 1.117 1.7021 3.1382 1.7021 1.3829 0 2.6595-0.69146 1.2765-0.74465 2.4999-1.5957 1.2234-0.85103 2.4467-1.5425 1.2234-0.74465 2.4467-0.74465zm19.786 0.58508q0 0.95741 0.21276 2.0212 0.21276 1.0106 0.63827 1.9148 0.42552 0.85103 1.117 1.4361 0.69146 0.58508 1.7021 0.58508 1.0638 0 1.6489-0.53189 0.63827-0.53189 0.95741-1.3829 0.31914-0.90422 0.42552-1.968 0.10638-1.117 0.10638-2.234t-0.26595-2.3403q-0.21276-1.2234-0.69146-2.234-0.42552-1.0638-1.117-1.7552-0.63827-0.69146-1.4893-0.69146-0.58508 0-1.1702 0.53189-0.58508 0.53189-1.0638 1.4893-0.42551 0.90422-0.74465 2.234-0.26595 1.3297-0.26595 2.9254zm2.9254 21.01q-3.9892 0-7.0742-1.117-3.085-1.117-5.2126-3.5105-2.0744-2.3935-3.1382-6.17-1.0638-3.7764-1.0638-9.0954 0-5.1062 1.3297-8.723 1.3829-3.6701 3.7233-6.0104 2.3935-2.3935 5.5849-3.4573 3.1914-1.117 6.9146-1.117 3.7764 0 6.8614 1.1702 3.1382 1.1702 5.3189 3.4573 2.234 2.2871 3.4573 5.7444 1.2234 3.4041 1.2234 7.9784 0 10.372-4.6275 15.638-4.5743 5.2126-13.297 5.2126zm46.913-32.02q0 1.2765-0.69146 2.9786-0.69146 1.7021-1.7552 3.5637-1.0106 1.8084-2.234 3.6169-1.1702 1.7552-2.234 3.1914-1.0106 1.4361-1.7021 2.3403t-0.69146 1.0106q0 0.42551 0.4787 0.4787 0.53189 0 0.79784 0 0.79784 0 1.3829 0 0.58508-0.05319 1.0638-0.05319 0.53189-0.05319 1.117-0.05319 0.58508-0.05319 1.3829-0.05319 1.9148 0 3.1382 0.42551 1.2765 0.37232 1.968 1.3297 0.74466 0.90422 1.0106 2.4467 0.26595 1.4893 0.26595 3.6701 0 2.4467-0.15957 3.7764-0.15957 1.3297-1.3297 1.9148-1.1702 0.58508-3.8296 0.69146-2.6595 0.10638-7.6061 0.10638-1.7021 0-3.4573-0.05319-1.7552-0.05319-3.4573-0.05319-2.0212 0-3.9892-0.21276-1.968-0.15957-3.5637-0.85103-1.5425-0.74465-2.5531-2.1808-0.95741-1.4361-0.95741-3.936 0-1.7021 1.0106-3.6701 1.0106-2.0212 2.4467-4.0424 1.4361-2.0212 2.9786-3.8828 1.5957-1.8616 2.7127-3.1914 0.26595-0.31914 0.79784-0.85103t1.0638-1.0638 0.90422-1.0106q0.37232-0.4787 0.31914-0.74465-0.05319-0.42552-0.26595-0.42552-0.15957-0.05319-0.26595-0.05319-1.6489 0-3.2977 0.26595-1.5957 0.26595-3.2445 0.26595-1.2234 0-2.0744-0.63827-0.85103-0.63827-1.3829-1.6489-0.4787-1.0638-0.74465-2.3935-0.21276-1.3297-0.21276-2.6595 0-1.968 1.3829-3.1914 1.4361-1.2234 3.4041-1.9148 2.0212-0.69146 4.202-0.90422 2.1808-0.26595 3.6701-0.26595 2.3403 0 4.8402 0.05319 2.4999 0.05319 4.5743 0.79784 2.0744 0.69146 3.4041 2.3403 1.3829 1.5957 1.3829 4.7338zm2.0742-0.58508q0-1.5425 0.26595-2.819 0.26594-1.2765 0.90422-2.1808 0.63827-0.90422 1.7552-1.3829 1.117-0.53189 2.819-0.53189 2.6595 0 3.936 0.69146 1.2766 0.63827 1.8084 2.4999 0.26595 0.85103 0.42551 1.4893 0.15957 0.58508 0.37233 1.0638 0.26595 0.4787 0.63827 0.95741 0.42552 0.42552 1.117 0.95741 0.26594 0.21276 0.58508 0.37232 0.31913 0.15957 0.69146 0.15957 2.1276 0 3.3509-1.1702 1.2765-1.1702 1.5425-3.1382 0.15957-1.3297 0.69147-2.1808 0.58508-0.85103 1.3829-1.3297 0.79784-0.53189 1.7021-0.69146 0.90421-0.21276 1.8084-0.21276 2.3403 0 3.7764 1.3829t1.4361 3.7764q0 2.6063-0.37232 4.6275-0.31914 2.0212-1.117 3.7764-0.74465 1.7021-2.0212 3.2977-1.2234 1.5957-2.9786 3.3509-1.7552 1.8084-2.2871 3.085-0.53189 1.2234-0.53189 1.7552 0 0.95741 0.10637 1.8084 0.15957 0.85103 0.26595 1.7021 0.15957 0.85103 0.26595 1.7552 0.15956 0.90422 0.15956 2.0212 0 1.4893-0.42551 2.819-0.37232 1.2765-1.2234 2.2871t-2.234 1.5957q-1.3829 0.58508-3.3509 0.58508-3.9892 0-5.7976-1.8084-1.8084-1.8084-1.8084-5.6913 0-1.0106 0.10638-1.7552 0.15957-0.79784 0.31913-1.4893 0.21276-0.74465 0.31914-1.5425 0.15957-0.79784 0.15957-1.8084 0-1.6489-0.74465-2.9254t-2.1276-2.6595q-2.5531-2.3935-4.1488-5.5849-1.5425-3.2445-1.5425-6.9146z"
stroke-width=".27703" aria-label="COZY" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

13
public/manifest.json Normal file
View file

@ -0,0 +1,13 @@
{
"name": "Cozy",
"short_name": "Cozy",
"icons": [
{
"src": "touch-icon-large.png",
"sizes": "500x500"
}
],
"background_color": "#ffffff",
"theme_color": "#3054bf",
"display": "fullscreen"
}

1
public/mask-icon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/touch-icon-large.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

107
purge-job.cron Normal file
View file

@ -0,0 +1,107 @@
```bash
#!/bin/bash
# Script: purge-cozy.sh
# Purpose: Clean up temporary files and logs at midnight
# This script should be scheduled as a cron job to run daily at midnight
# Set working directory
cd /path/to/your/script/directory || exit 1
# Define log file
LOG_FILE="/var/log/purge-cozy.log"
# Function to log messages
log_message() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"
}
# Start logging
log_message "Starting purge-cozy.sh script execution"
# Remove temporary files older than 7 days
log_message "Removing temporary files older than 7 days"
find /tmp -type f -mtime +7 -delete 2>>"$LOG_FILE" || log_message "Warning: find command failed"
# Clean up log files older than 30 days
log_message "Cleaning up log files older than 30 days"
find /var/log -name "*.log" -mtime +30 -delete 2>>"$LOG_FILE" || log_message "Warning: log cleanup failed"
# Remove cache directories (example for user cache)
log_message "Clearing user cache directories"
find /home -type d -name ".cache" -mtime +7 -exec rm -rf {} + 2>>"$LOG_FILE" || log_message "Warning: cache cleanup failed"
# Optional: Clean up specific application directories
# log_message "Cleaning up cozy application data"
# find /var/lib/cozy -type f -mtime +30 -delete 2>>"$LOG_FILE" || log_message "Warning: cozy data cleanup failed"
# Remove empty directories (excluding important system directories)
log_message "Removing empty directories"
find /tmp -type d -empty -delete 2>>"$LOG_FILE" || log_message "Warning: empty directory cleanup failed"
# Display disk usage after cleanup
log_message "Disk usage after cleanup:"
df -h | tee -a "$LOG_FILE"
# End logging
log_message "purge-cozy.sh script execution completed"
```
To set up the cron job:
1. First, make your script executable:
```bash
chmod +x purge-cozy.sh
```
2. Edit your crontab:
```bash
sudo crontab -e
```
3. Add this line to run the script every midnight:
```bash
0 0 * * * /path/to/purge-cozy.sh
```
4. Save and exit (in nano editor: Ctrl+X, then Y, then Enter)
Alternative method using systemd timer (modern approach):
```bash
# Create a service file: /etc/systemd/system/purge-cozy.service
[Unit]
Description=Clean up temporary files and logs
After=network.target
[Service]
Type=oneshot
ExecStart=/path/to/purge-cozy.sh
User=root
```
```bash
# Create a timer file: /etc/systemd/system/purge-cozy.timer
[Unit]
Description=Run purge-cozy script daily at midnight
Requires=purge-cozy.service
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target
```
```bash
# Enable and start the timer
sudo systemctl enable purge-cozy.timer
sudo systemctl start purge-cozy.timer
```
Key considerations:
- Replace `/path/to/your/script/directory` with actual path
- Adjust cleanup rules based on your specific needs
- Ensure script has proper permissions to access directories
- Test script manually before scheduling it
- Monitor log file for any errors during execution

41
server.mjs Executable file
View file

@ -0,0 +1,41 @@
#!/usr/bin/env node
import Fastify from 'fastify'
import fastifyMiddie from '@fastify/middie'
import fastifyStatic from '@fastify/static'
import { fileURLToPath } from 'node:url'
import { handler as ssrHandler } from './dist/server/entry.mjs'
const app = Fastify({ logger: true })
await app
.register(import('@fastify/rate-limit'), {
global: true,
max: 25,
timeWindow: 1000 * 60 * 5,
})
.register(fastifyStatic, {
root: fileURLToPath(new URL('./dist/client', import.meta.url)),
})
.register(fastifyMiddie)
app.use(ssrHandler)
await app.setNotFoundHandler(
{
preHandler: app.rateLimit(),
},
function (request, reply) {
reply.code(404).send({ nothing: 'to see here' })
}
)
await app.setErrorHandler(function (error, request, reply) {
if (error.statusCode === 429) {
reply.code(429)
error.message = 'You hit the rate limit! Slow down please!'
}
reply.send(error)
})
app.listen({ port: 4321 })

View file

@ -1,80 +1,136 @@
---
import Icon from 'astro-iconify'
export interface Props {
url: string;
url: string | null
}
const { url } = Astro.props;
const placeholder = 'Type the article URL here'
const { url } = Astro.props
---
<div class="address-bar">
<div id="address-bar">
<form>
<button class="left-button" type="button" id="app-home" name="app-home" onclick="window.location.href = '/';">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M17.51 3.87L15.73 2.1L5.84 12l9.9 9.9l1.77-1.77L9.38 12l8.13-8.13z"/></svg>
</button>
<input type="text" id="app-url" name="url" value={url} placeholder="Type a URL here" />
<button class="right-button" type="submit" id="submit">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M6.23 20.23L8 22l10-10L8 2L6.23 3.77L14.46 12z"/></svg>
</button>
<button class="right-button" type="button" id="gh-link" onclick="window.location.href = 'https://github.com/ayoayco/cozy'">
<svg xmlns="http://www.w3.org/2000/svg" width="1227.83" height="1000" viewBox="0 0 1227.825 1000"><path fill="currentColor" d="M1078.94-.985c-33.192-.491-110.295 10.777-239.027 96.936c-70.161-17.535-144.812-26.188-219.591-26.188c-82.278 0-165.425 10.448-242.965 31.719C192.534-24.605 110.955 1.234 110.955 1.234c-53.258 133.183-20.347 231.788-10.344 256.277C38.014 325.069-.2 411.338-.2 517.07c0 79.822 9.085 151.416 31.281 213.653c1.231 4.803.832 3.732 2.906 7.844c4.89 12.884 10.327 25.39 16.438 37.468c2.094 4.346 4 7.563 4 7.563c62.395 116.307 185.396 191.438 404.244 215.028l330.995.375c233.392-23.144 345.386-98.499 396.994-215.591l3.281-7.625c4.89-11.828 9.153-24.135 20.813-65.562c11.659-41.427 16.875-113.172 16.875-193.185c0-114.755-43.1-206.577-113.092-276.434c12.231-39.48 28.57-127.158-16.313-239.402c0 0-6.293-1.995-19.281-2.188zM818.1 420.133c53.893-.117 100.057 9.136 134.717 45.499v.031c43.369 45.541 68.749 100.525 68.749 159.778c0 276.658-177.932 284.183-397.4 284.183c-219.506 0-397.4-38.336-397.4-284.183c0-58.861 25.009-113.516 67.843-158.872c71.451-75.59 192.365-35.562 329.558-35.562c70.423-.011 136.564-10.75 193.935-10.875zm-408.807 81.468c-45.666 0-82.687 61.741-82.687 137.936c0 76.206 37.019 137.967 82.687 137.967c45.666 0 82.687-61.761 82.687-137.967c0-76.184-37.019-137.881-82.687-137.936zm443.649 0c-45.666 0-82.687 61.741-82.687 137.936c0 76.206 37.019 137.967 82.687 137.967c45.666 0 82.687-61.761 82.687-137.967c0-76.184-37.019-137.881-82.687-137.936z"/></svg>
<label for="app-url">
<Icon name="ic:round-arrow-forward-ios" />
</label>
<input
type="url"
id="app-url"
name="url"
value={url ?? ''}
placeholder={placeholder}
required
/>
<button
aria-label="Submit"
class="btn right-buttons primary"
type="submit"
id="submit"
>
<Icon name="ri:ai-generate" />
</button>
<a
aria-label="Home"
class="btn right-buttons"
type="button"
id="app-home"
href="/"
>
<Icon name="mdi:home" />
</a>
</form>
</div>
<script>
const homeBtn = document.getElementById('app-home') as HTMLButtonElement;
const urlInput = document.getElementById('app-url') as HTMLInputElement;
if (urlInput.value === '')
homeBtn.setAttribute('style', 'display: none');
</script>
<style lang="scss">
.address-bar {
text-align: center;
<style>
#address-bar {
width: 100%;
position: relative;
}
:global(form) {
form:has(input[type='url']:focus) {
border-color: var(--accent);
box-shadow: 0 1px 10px 0px var(--accent);
}
form {
width: 100%;
border: 0px;
padding: 0.5rem;
padding: 0.5rem 1rem;
text-align: center;
border-radius: 5px;
border: 1px solid #ccc;
background-color: #f5f5f5;
box-shadow: 0 1px 3px 1px #eee;
border-radius: 30px;
border: 2px solid rgb(var(--gray));
background-color: white;
box-shadow: 0 1px 3px 1px rgb(var(--gray-light));
display: flex;
:global(input[type="text"]) {
flex: 3;
background-color: white;
border-radius: 5px;
border: 1px solid #eee;
padding: 0.5rem;
input[type='url']:focus {
outline: none;
}
:global(button#app-home),
:global(button#submit),
:global(button#gh-link) {
input[type='url'] {
flex: 3;
border: 0px;
background-color: transparent;
padding: 0px;
cursor: pointer;
color: #888;
border-radius: 30px;
font-size: normal;
padding: 0.5rem;
color: rgb(var(--black));
caret-color: var(--accent);
}
:global(svg) {
border: 0px;
background-color: transparent;
width: 1.5rem;
height: 1.5rem;
label {
display: block;
border: 0px;
height: 100%;
vertical-align: middle;
background-color: transparent;
padding: 0.5rem 0;
color: rgb(var(--gray));
svg {
border: 0px;
background-color: transparent;
width: 24px;
height: 24px;
cursor: pointer;
}
}
:global(.left-button) {
.btn.primary {
color: var(--accent);
}
.btn {
color: rgb(var(--gray));
display: block;
border: 0px;
height: 100%;
vertical-align: middle;
background-color: transparent;
padding: 0.5rem 0;
svg {
border: 0px;
background-color: transparent;
width: 24px;
height: 24px;
cursor: pointer;
}
}
.left-buttons {
margin-right: 0.5rem;
}
:global(.right-button) {
.right-buttons {
margin-left: 0.5rem;
}
.btn svg:hover {
color: blue !important;
}
.btn[disabled='true'] svg {
color: rgb(var(--gray-light)) !important;
cursor: default !important;
}
}
</style>

View file

@ -0,0 +1,66 @@
---
import Icon from 'astro-iconify'
import { VERSION } from '../consts'
---
<footer>
<section>Remove distractions. Save for later.</section>
<section class="attribution">
<a href="/blog/01-building-a-cozy-web/">Hand-crafted</a> with <Icon
name="line-md:heart"
/> by <a href="https://ayo.ayco.io">Ayo Ayco</a>
<br />
<a href="/blog">Blog</a> •
<a href="https://ayco.io/sh/cozy">SourceHut</a> •
<a href="https://social.ayco.io/@ayo">Mastodon</a>
<br />
<span>{VERSION}</span>
</section>
<section class="disclaimer">All rights reserved to content owners.</section>
</footer>
<style>
footer {
width: 100%;
margin: 0 auto;
text-align: center;
color: rgb(var(--gray));
display: flex;
font-size: small;
& a {
color: rgb(var(--gray));
}
& a:hover {
color: var(--accent);
}
& svg {
width: 1.25rem;
height: 1.25rem;
vertical-align: bottom;
display: inline;
border: 0px;
color: red;
}
& section {
flex: 1;
padding: 1rem 1rem 0;
&.attribution {
flex: 2;
}
}
}
@media (max-width: 600px) {
footer {
flex-direction: column;
max-width: 350px;
}
}
</style>

View file

@ -0,0 +1,25 @@
---
const { default: innerHTML } = await import(`/public/jumbotron.svg?raw`)
---
<header id="jumbotron">
<a href="/">
<Fragment set:html={innerHTML} />
</a>
</header>
<style>
#jumbotron {
margin: 0 auto;
svg {
border: 0px;
width: 300px;
max-width: 100%;
}
&:hover {
filter: var(--svg-filter-accent);
}
}
</style>

View file

@ -1,162 +1,232 @@
---
import Serialize from '@ayco/astro-resume'
export interface Props {
postDivSelector: string,
skipSave?: boolean
}
const {postDivSelector, skipSave = false} = Astro.props;
---
<div id="library">
<span id="heading"></span>
<ul id="post-list"></ul>
</div>
<input value={postDivSelector} name="postDivSelector" id="postDivSelector" hidden />
<input type="checkbox" id="skipSave" name="skipSave" checked={skipSave} hidden />
<Serialize id="preferences" data={Astro.props} />
<script>
import { getPostCard } from '../utils/library'
const cache = await caches.open('cozy-reader');
const url = new URL(window.location.href);
const response = await cache.match(url)
const postDivSelector = document.getElementById('postDivSelector') as HTMLInputElement;
const skipSave = document.getElementById('skipSave') as HTMLInputElement;
const postDiv = document.querySelector(postDivSelector?.value);
if (!response) {
if (!skipSave?.checked) {
await cache.add(url);
}
import { getPostCard, renderPost } from '../utils/library'
import { cozify } from '../utils/sanitizer'
import { deserialize } from '@ayco/astro-resume'
const cache = await caches.open('cozy-reader')
const baseUrl = window.location.origin + '/a'
let url = new URL(window.location.href)
// only cached unencoded url param
const urlParam = url.searchParams.get('url')
if (urlParam) {
url = new URL(`${url.origin}/?url=${urlParam}`)
}
const cachedRequests = await cache.keys();
const list = document.querySelector('#post-list');
const { skipSave } = deserialize('preferences') ?? ''
if(cachedRequests?.length) {
const heading = document.querySelector('#library span#heading') as HTMLHeadingElement;
heading.innerHTML = 'History';
const routerOutlet = 'router-outlet'
const includesAppURL = urlParam?.includes(baseUrl) ?? false
cachedRequests
// temporary delete all cached errors
.filter(request => {
const urlObj = new URL(request.url);
return urlObj.search !== '';
try {
if (
url.href.slice(0, url.href.length - 1) !== baseUrl &&
!skipSave &&
!includesAppURL
) {
console.info('adding one to cache', {
context: 'cozy-reader',
data: url,
})
await cache.add(url)
}
} catch (error) {
console.error('ERR', { context: 'cozy-reader', data: error })
}
const cachedRequests = (await cache.keys()).filter((request) => {
const urlObj = new URL(request.url)
const urlParam = urlObj.searchParams.get('url')
return (
urlObj.search !== '' &&
!urlParam?.startsWith(baseUrl) &&
urlParam !== '' &&
urlParam !== 'null'
)
})
if (cachedRequests?.length && routerOutlet !== null) {
const list = document.querySelector('#post-list')
const heading = document.querySelector(
'#library span#heading'
) as HTMLHeadingElement
heading.innerHTML = 'History'
cachedRequests.reverse().forEach(async (request) => {
const { url } = request
const link = document.createElement('a')
let responseText
const fullResponse = await cache.match(url)
if (
!fullResponse &&
url.slice(0, url.length - 1) !== baseUrl &&
!skipSave &&
!includesAppURL
) {
console.info('updating cached', { context: 'cozy-reader', data: url })
await cache.add(url)
}
fullResponse?.text().then(async (data) => {
responseText = data
const cleanedResponse = await cozify(responseText, baseUrl)
const html = document.createElement('html')
html.innerHTML = cleanedResponse
const title = html
.querySelector('meta[property="cozy:title"]')
?.getAttribute('content')
if (title === 'Something is not right') {
cache.delete(url)
return // temporary fix for deleting cached errors
}
const postCard = getPostCard(html)
link.innerHTML = postCard
link.href = url
link.onclick = async (e) => {
e.preventDefault()
localStorage.setItem('scrollPosition', window.scrollY.toString())
scrollTo(0, 0)
console.info('using cached response', {
context: 'cozy-reader',
data: url,
})
renderPost(cleanedResponse, url, routerOutlet)
}
const item = document.createElement('li')
item.appendChild(link)
list?.appendChild(item)
})
})
.reverse()
.forEach(async (request) => {
const {url} = request;
const link = document.createElement('a');
let responseText;
window.addEventListener('popstate', async (data) => {
let url = data.state?.url
let isHome = false
if (!url) {
url = window.location.href
isHome = true
} else {
// replace scrollPosition
localStorage.setItem('scrollPosition', window.scrollY.toString())
}
const fullResponse = await cache.match(url)
fullResponse?.text().then(data => {
responseText = data;
const html = document.createElement('html');
html.innerHTML = responseText;
const title = html.querySelector('meta[property="cozy:title"]')?.getAttribute('content');
if (title === 'Something is not right') {
cache.delete(url);
return; // temporary fix for deleting cached errors
fullResponse?.text().then(async (data) => {
const responseText = data
const cleanedResponse = await cozify(responseText, baseUrl)
console.info('using cached response', {
context: 'cozy-reader',
data: url,
})
renderPost(cleanedResponse, url, routerOutlet, true)
if (isHome) {
const scrollPosition = localStorage.getItem('scrollPosition')
scrollTo(0, scrollPosition ? parseInt(scrollPosition) : 0)
}
const postCard = getPostCard(html);
link.innerHTML = postCard;
link.href = url;
link.onclick = async (e) => {
e.preventDefault();
scroll(0,0);
const html = document.createElement('html');
html.innerHTML = responseText;
const newPost = html.querySelector('body')?.querySelector('#post');
if (postDiv && newPost?.innerHTML) {
postDiv.innerHTML = newPost.innerHTML
const homeBtn = document.getElementById('app-home') as HTMLButtonElement;
homeBtn.setAttribute('style', 'display: block');
}
}
const item = document.createElement('li');
item.appendChild(link);
list?.appendChild(item);
});
});
})
})
}
</script>
<style lang="scss">
<style>
#library {
:global(span#heading) {
span#heading {
color: #555;
font-size: small;
text-transform: uppercase;
}
}
#post-list {
list-style: none;
padding-left: 0;
display: grid;
gap: 1em;
:global(li) {
list-style: none;
width: calc(100% + 40px);
margin-left: -40px;
:global(a) {
li {
a {
text-decoration: none;
color: #000;
:global(h3) {
h3 {
text-decoration: underline;
}
:global(.post-card) {
padding-bottom: 1rem;
.post-card {
display: grid;
grid-template-columns: calc(70px + 0.5em) auto;
:global(.post-card__image) {
float: left;
margin: 0.25rem 0.5rem 0.25rem 0;
:global(img, svg) {
.post-card__image {
img,
svg {
width: 70px;
height: 70px;
object-fit: cover;
border-radius: 5px;
border: 1px solid #eee;
border: 2px solid rgb(var(--gray));
background-color: rgb(var(--gray));
margin: 0.15rem 0;
}
:global(svg) {
color: #ccc;
padding: 0.5rem;
svg {
color: rgb(var(--gray-light));
padding: 0.5em;
}
}
}
:global(.post-card__content) {
display: flex;
flex-direction: column;
justify-content: center;
min-height: calc(70px + 0.5rem);
}
:global(.post-card__title, .post-card__description) {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
:global(.post-card__meta, .post-card__description){
font-size: smaller;
color: #555;
}
:global(.post-card__meta) {
display: flex;
justify-content: space-between;
* {
flex: 1;
.post-card__content {
display: flex;
flex-direction: column;
justify-content: center;
min-height: calc(70px + 0.5rem);
}
:global(.post-card__source) {
font-weight: bold;
.post-card__title,
.post-card__description {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.post-card__meta,
.post-card__description {
font-size: smaller;
color: #555;
}
.post-card__meta {
display: flex;
justify-content: space-between;
margin-top: 0;
* {
flex: 1;
}
.post-card__source {
font-weight: bold;
}
.post-card__published {
text-align: right;
}
}
}
}
}
}
</style>
</style>

View file

@ -1,76 +1,112 @@
---
import { ArticleData } from "@extractus/article-extractor";
import { ArticleData } from '@extractus/article-extractor'
import { cozify } from '../utils/sanitizer'
export interface Props {
article: ArticleData;
article: ArticleData | null
}
const { article } = Astro.props;
const error: ArticleData = {
title: 'Something is not right',
content: '<p>The article extractor did not get any information.</p>',
}
let { article } = Astro.props
article ??= error
const datePublished =
article?.published && new Date(article.published).toDateString();
article?.published && new Date(article.published).toDateString()
const cleanContent = await cozify(article.content ?? '', Astro.url.origin)
---
<div id="post">
{
article && article.url !== '/' &&
<>
{article.source && <span class="source">{article.source}</span>}
{article.title && <h1 class="title">{article.title}</h1>}
{(article.author || datePublished) && (
<ul class="publish-info">
{article.author && <li>{article.author} </li>}
{datePublished && <li>{datePublished}</li>}
</ul>
)}
<content set:html={article.content} />
</>
}
</div>
<style is:global lang="scss">
@counter-style publish-icons {
system: cyclic;
symbols: "️✍️" "🗓️";
suffix: " ";
}
<!--
Changing anything inside the #post div can cause a difference in the cached version of the post in users' devices. For this reason, we should avoid changing the HTML and instead do it with CSS when possible.
-->{
article && article.url !== '/' && (
<article id="post">
{article.source && <span class="source">{article.source}</span>}
{article.title && <h1 class="title">{article.title}</h1>}
{(article.author || datePublished) && (
<ul class="publish-info">
{article.author && <li>{article.author} </li>}
{datePublished && <li>{datePublished}</li>}
</ul>
)}
<content set:html={cleanContent} />
</article>
)
}
<style>
#post {
h1.title {
font-size: xx-large;
margin: 0;
}
span.source {
font-weight: bolder;
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.2;
}
.source,
.publish-info {
font-size: smaller;
color: #555;
}
ul.publish-info {
margin: 0.3em -0.7em 1em;
list-style: publish-icons;
.source {
font-weight: bold;
}
.publish-info {
padding-left: 0;
margin: 0;
list-style: none;
li {
color: #555;
font-size: small;
margin: 0;
}
}
content {
p, table, ul, img {
margin: 1em 0 !important;
font-size: 20px;
}
table {
border-collapse: collapse;
td, th {
border: 1px solid #ccc;
padding: 0.5em;
content {
p,
table,
ul,
img {
margin: 1em 0 !important;
font-size: 20px;
}
}
pre {
white-space: pre-wrap;
table {
border-collapse: collapse;
td,
th {
border: 1px solid #ccc;
padding: 0.5em;
}
}
pre {
white-space: pre-wrap;
&:has(code) {
padding: 1em;
background: #f6f8fa;
border-radius: 5px;
}
}
@media (max-width: 600px) {
p,
table,
ul,
img {
font-size: 16px;
}
}
}
}
</style>
</style>

View file

@ -0,0 +1,67 @@
---
import '../../styles/reset.css'
import '../../styles/variables.css'
import '../../styles/blog.css'
import { SITE_TITLE, SITE_AUTHOR, SITE_DESCRIPTION } from '../../consts'
interface Props {
title: string
description: string
isArticle?: boolean
}
let {
isArticle = false,
title,
description = 'default description',
} = Astro.props
description =
title === SITE_TITLE ? SITE_DESCRIPTION : `${description} • ${SITE_TITLE}`
---
<!-- Global Metadata -->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<!-- Icons -->
<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" />
<meta name="generator" content={Astro.generator} />
<!-- Font preloads -->
<link
rel="preload"
href="/fonts/atkinson-regular.woff"
as="font"
type="font/woff"
crossorigin
/>
<link
rel="preload"
href="/fonts/atkinson-bold.woff"
as="font"
type="font/woff"
crossorigin
/>
<!-- Primary Meta Tags -->
<title>{title} • {description}</title>
<meta name="title" content={title} />
<meta name="description" content={description} />
<!-- Open Graph / Facebook -->
{
isArticle ? (
<meta property="og:type" content="article" />
) : (
<meta property="og:type" content="website" />
)
}
<meta property="og:url" content={Astro.url} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content="/touch-icon-large.png" />
<meta property="og:site_name" content={SITE_TITLE} />
<meta property="article:author" content={SITE_AUTHOR} />

View file

@ -0,0 +1,81 @@
---
import {
SITE_AUTHOR,
SITE_AUTHOR_EMAIL,
SITE_AUTHOR_MASTODON,
SITE_PROJECT_REPO,
} from '../../consts'
const today = new Date()
---
<footer>
<p>&copy; {today.getFullYear()} {SITE_AUTHOR}. All rights reserved.</p>
<p>
Want to get in touch? Send a mail to <a href={`mailto:${SITE_AUTHOR_EMAIL}`}
>Cozy at ayco.io</a
>.
</p>
<div class="social-links">
<a href={SITE_AUTHOR_MASTODON} target="_blank">
<span class="sr-only">Follow Ayo on Mastodon</span>
<svg
viewBox="0 0 16 16"
aria-hidden="true"
width="32"
height="32"
astro-icon="social/mastodon"
><path
fill="currentColor"
d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a3.614 3.614 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522c0-.859.22-1.541.66-2.046.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764.442.505.661 1.187.661 2.046v4.203z"
></path></svg
>
</a>
<a href={SITE_PROJECT_REPO} target="_blank">
<span class="sr-only">Go to Cozy's GitHub repo</span>
<svg
viewBox="0 0 16 16"
aria-hidden="true"
width="32"
height="32"
astro-icon="social/github"
><path
fill="currentColor"
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"
></path></svg
>
</a>
</div>
</footer>
<style>
footer {
padding: 2em 1em 6em 1em;
background: linear-gradient(var(--gray-gradient)) no-repeat;
color: rgb(var(--gray));
text-align: center;
& p {
margin-bottom: 0;
& a {
color: rgb(var(--gray));
&:hover {
color: rgb(var(--gray-dark));
}
}
}
}
.social-links {
display: flex;
justify-content: center;
gap: 1em;
margin-top: 1em;
}
.social-links a {
text-decoration: none;
color: rgb(var(--gray));
}
.social-links a:hover {
color: rgb(var(--gray-dark));
}
</style>

View file

@ -0,0 +1,17 @@
---
interface Props {
date: Date
}
const { date } = Astro.props
---
<time datetime={date.toISOString()}>
{
date.toLocaleDateString('en-us', {
year: 'numeric',
month: 'short',
day: 'numeric',
})
}
</time>

View file

@ -0,0 +1,140 @@
---
import {
SITE_AUTHOR_MASTODON,
SITE_DESCRIPTION,
SITE_PROJECT_REPO,
SITE_TITLE,
} from '../../consts'
---
<header>
<nav>
<div class="site-title">
<h2><a href="/blog">{SITE_TITLE}</a></h2>
<small class="site-description">{SITE_DESCRIPTION}</small>
</div>
<div class="social-links">
<a href="/">
<span class="primary-btn">Get Cozy!</span>
</a>
<a href={SITE_AUTHOR_MASTODON} target="_blank">
<span class="sr-only">Follow Ayo on Mastodon</span>
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32"
><path
fill="currentColor"
d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a3.614 3.614 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522c0-.859.22-1.541.66-2.046.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764.442.505.661 1.187.661 2.046v4.203z"
></path></svg
>
</a>
<a href={SITE_PROJECT_REPO} target="_blank">
<span class="sr-only">Go to Cozy's GitHub repo</span>
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32"
><path
fill="currentColor"
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"
></path></svg
>
</a>
</div>
</nav>
</header>
<style>
header {
margin: 0;
padding: 0 1em;
background: white;
box-shadow: 0 2px 8px rgba(var(--black), 5%);
width: 100%;
}
h2 {
margin: 0;
font-size: x-large;
}
h2 a,
h2 a.active {
text-decoration: none;
}
nav {
width: 900px;
max-width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: 1em 0.5em;
margin: 0 auto;
& span.primary-btn {
background-color: rgba(var(--black), 95%);
box-shadow: 0 2px 8px rgba(var(--black), 5%);
color: white;
border-radius: 5px;
display: inline-block;
text-align: center;
padding: 1px 0.5em 0;
transition: 0.2s ease;
}
}
nav .social-links a:hover {
& span {
background-color: var(--accent);
box-shadow: 0 2px 8px var(--accent);
}
}
nav .site-title {
display: flex;
}
nav a,
nav .site-description {
color: var(--black);
border-bottom: 4px solid transparent;
text-decoration: none;
}
nav a {
padding: 0 0.5em;
}
nav a:hover {
color: var(--accent);
}
nav a.active {
text-decoration: none;
border-bottom-color: var(--accent);
}
.social-links,
.social-links a {
display: flex;
}
@media (max-width: 700px) {
nav {
display: block;
padding: 1em 0;
& .site-description {
font-size: 1rem;
}
& span.primary-btn {
line-height: 1.5rem;
height: 1.5rem;
}
& .social-links a {
font-size: small;
svg {
height: 1.5rem;
}
}
}
nav a {
padding: 0.5em;
padding-left: 0;
}
}
</style>

View file

@ -0,0 +1,26 @@
---
import type { HTMLAttributes } from 'astro/types'
type Props = HTMLAttributes<'a'>
const { href, class: className, ...props } = Astro.props
const { pathname } = Astro.url
// eslint-disable-next-line no-useless-escape
const subpath = pathname.match(/[^\/]+/g)
const isActive = href === pathname || href === '/' + subpath?.[0]
---
<a href={href} class:list={[className, { active: isActive }]} {...props}>
<slot />
</a>
<style>
a {
display: inline-block;
text-decoration: none;
}
a.active {
font-weight: bolder;
text-decoration: underline;
}
</style>

9
src/consts.ts Normal file
View file

@ -0,0 +1,9 @@
export const SITE_TITLE = 'Cozy Blog'
export const SITE_AUTHOR = 'Ayo Ayco'
export const SITE_AUTHOR_URL = 'https://ayo.ayco.io'
export const SITE_AUTHOR_EMAIL = 'cozy@ayco.io'
export const SITE_AUTHOR_MASTODON = 'https://social.ayco.io/@ayo'
export const SITE_PROJECT_REPO = 'https://github.com/ayoayco/Cozy'
export const SITE_DESCRIPTION = 'The Web is Yours.'
export const VERSION = 'Drooling-Dogs'

17
src/content.config.ts Normal file
View file

@ -0,0 +1,17 @@
import { glob } from 'astro/loaders'
import { defineCollection, z } from 'astro:content'
const blog = defineCollection({
loader: glob({ base: './src/content/blog', pattern: '**/*.md' }),
// Type-check frontmatter using a schema
schema: z.object({
title: z.string(),
description: z.string(),
// Transform string to Date object
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: z.string().optional(),
}),
})
export const collections = { blog }

View file

@ -0,0 +1,67 @@
---
title: Building a Cozy Web
description: Let us build the web we want!
pubDate: 'Aug 14 2024'
heroImage: '/cozy.jpg'
---
> This was originally posted on [Ayo's Blog](https://ayos.blog/building-a-cozy-web) last Jun 1, 2023.
Have you ever clicked a link to an article, all hyped up to read the content, only to be slapped in the face with popups over popups of requests to subscribe and asking consent to track you with cookies?
Do you sometimes wish you can have a consistent experience when opening articles... a place to save all your favorites, and possibly get helpful insights?
Ah, well you're not alone. 🤣
This is exactly why I started [**Cozy** 🧸](https://cozy.pub/).
It's a simple web page that can make any web page content-focused! 🎉
It uses a library called [@extractus/article-extractor](https://www.npmjs.com/package/@extractus/article-extractor) to fetch and extract just the content.
Then with [Astro](https://astro.build), we can server-side render the page so your browser only gets clean HTML!
No nonsense. No headaches.
The project and the road map for features are all public on my [GitHub](https://github.com/ayoayco/cozy-reader)
## Cozy Features
Right now, it successfully extracts the content and delivers a clean page to your browser.
I'm working toward bringing the following in the coming weeks:
1. Save favorites to a library
2. Offline access
3. Smart Insights about the article
4. Easier usage (browser extensions or apps?)
## Coziest Usage
The most convenient way to use it right now is through what we call a browser bookmarklet.
Basically you can have a button there beside your other bookmarks that will open the current page in Cozy.
You can create this new bookmark titled 'Get cozy!' and put the following as value for the URL:
```js
javascript:(function(){ window.open('https://cozy.pub/?url=%27 + window.location.href, %27_self%27); })();
```
This is possible on all major browsers, including Safari on iOS (where I personally use this often). Some screenshots:
| Firefox | Chrome |
| ---------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| ![Screenshot from 2023-05-13 08-31-41](https://github.com/ayoayco/cozy-reader/assets/4262489/9b296d4f-2722-483a-bbc2-431c6b2ae996) | ![Screenshot from 2023-05-12 23-32-08](https://github.com/ayoayco/cozy-reader/assets/4262489/144b74f8-3949-46b9-849c-351e4af0ac12) |
## Join the Project!
I'm sure this looks very simple, but I think this is the most exciting hobby project I've started yet.
There's a lot that happened and a lot of problems could have been avoided if people were equipped to assess the content they find online.
I think there's lots of good a simple tool could bring if it allows users to cut-through all the distractions and are presented with unbiased and accurate information.
This project is a groundwork for this experience.
Let's build the web we want! 🧸

View file

@ -0,0 +1,21 @@
---
title: A Blog About the Blog About Cozy
description: Blogging about what the blog about Cozy is all about
pubDate: 'Aug 16 2024'
---
About a year ago, I briefly wrote [about Cozy on my personal blog](https://ayos.blog/building-a-cozy-web). I typically aim to do that every time I have a new hobby project to describe the motivations and enumerate the different tech that went into its creation.
At the time, Cozy was just another fun weekend project I built as I played around some web development techniques that were new to me. I listed some libraries I used, like [@extractus/article-extractor](https://www.npmjs.com/package/@extractus/article-extractor) and [Astro](https://astro.build), and vaguely mentioned the process that happens when you use the web app.
Since then, I've been using Cozy almost every time I read an article online. I have come to love the feeling of control, privacy, and ownership it gives -- something we have lost in almost all "modern" online experiences nowadays.
You visit a news website, for example, and you just know the content are mostly just a bait
Browsers are not helping. AI
Having a web page let's me skip all the noise that plague almost all modern websites
[Astro's on-demand rendering](https://docs.astro.build/en/guides/server-side-rendering/) and [JavaScript's Cache API](https://developer.mozilla.org/en-US/docs/Web/API/Cache)
The goal for this site is to have a place where I publish about the new features of the [web app](/) and the web development techniques used to achieve them.

View file

@ -0,0 +1,11 @@
---
title: Quivering Quacks
description: Cozy 0.3.2 Updates!
pubDate: 'Aug 19 2024'
---
<!--
New features since Jun 1 2023 -- 😄
-->
Ideally, I will have a post for each new app version deployed--for which I decided to do a naming convention: two words that [alliterate](https://cozy.pub/?url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FAlliteration), the first one being an adjective and the second a noun (e.g., <CurrentVersion />).

2
src/env.d.ts vendored
View file

@ -1 +1 @@
/// <reference types="astro/client" />
/// <reference path="../.astro/types.d.ts" />

140
src/layouts/App.astro Normal file
View file

@ -0,0 +1,140 @@
---
import Jumbotron from '../components/Jumbotron.astro'
import { ArticleData } from '@extractus/article-extractor'
import '../styles/reset.css'
import '../styles/variables.css'
export interface Props {
article: ArticleData | null
}
const { article } = Astro.props
const appTitle = article?.title ? `${article.title} | Cozy` : 'Cozy'
const siteName = 'cozy.pub'
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{appTitle}</title>
{
/**
* if showing a post:
* - don't allow search engines to index the page
* - add cozy metadata for the app to use
*/
article && article?.url !== '/' ? (
<>
<meta name="robots" content="noindex" />
<meta name="googlebot" content="noindex" />
<meta property="article:author" content={article.author} />
<meta property="og:description" content={article.description} />
<meta name="description" content={article.description} />
<meta itemprop="description" content={article.description} />
<meta property="cozy:title" content={article.title} />
<meta property="cozy:url" content={article.url} />
<meta property="cozy:description" content={article.description} />
<meta property="cozy:image" content={article.image} />
<meta property="cozy:source" content={article.source} />
<meta property="cozy:author" content={article.author} />
<meta property="cozy:published" content={article.published} />
</>
) : (
<>
<meta property="article:author" content="Ayo Ayco" />
<meta
property="og:description"
content="Remove distractions. Save your favorites. Get useful insights. Cozy is your modern-day reading companion."
/>
<meta
name="description"
content="Remove distractions. Save your favorites. Get useful insights. Cozy is your modern-day reading companion."
/>
<meta
itemprop="description"
content="Remove distractions. Save your favorites. Get useful insights. Cozy is your modern-day reading companion."
/>
</>
)
}
<meta property="og:title" content={appTitle} />
<meta property="og:url" content={Astro.url.href} />
<meta property="og:image" content="/touch-icon-large.png" />
<meta property="og:site_name" content={siteName} />
<!-- Icons -->
<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" />
</head>
<body>
<div id="app-wrapper">
<Jumbotron />
<slot />
<div id="main-content">
<div id="post-wrapper">
<slot name="post" />
</div>
<div id="library-wrapper">
<slot name="library" />
</div>
</div>
<slot name="footer" />
</div>
</body>
</html>
<style>
body {
display: grid;
place-content: safe center;
}
#app-wrapper {
padding: 0.5em 0.5em 10em;
--app-width: 650px;
max-width: var(--app-width);
width: 100%;
display: grid;
gap: 1em;
#main-content {
max-width: calc(var(--app-width) - 2em);
padding: 0 1em;
@media (max-width: 650px) {
max-width: calc(100vw - 2em);
padding: 0;
}
& table {
overflow-x: auto;
display: block;
}
}
&:has(#router-outlet #post) {
#jumbotron {
display: none;
}
}
}
</style>
<style is:global>
:root {
--system-ui:
system-ui, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
}
html * {
font-family: var(--system-ui);
}
</style>

178
src/layouts/Blog.astro Normal file
View file

@ -0,0 +1,178 @@
---
import BaseHead from '../components/blog/BaseHead.astro'
import Header from '../components/blog/Header.astro'
import Footer from '../components/blog/Footer.astro'
import FormattedDate from '../components/blog/FormattedDate.astro'
import type { CollectionEntry } from 'astro:content'
import { SITE_AUTHOR, SITE_AUTHOR_URL, SITE_AUTHOR_EMAIL } from '../consts'
type Props = CollectionEntry<'blog'>['data']
const { title, description, pubDate, updatedDate, heroImage } = Astro.props
---
<html lang="en">
<head>
<BaseHead title={title} description={description} isArticle={true} />
<style>
main {
width: calc(100% - 2em);
max-width: 100%;
margin: 0 auto;
}
& .cta-wrapper {
width: 300px;
max-width: 100%;
text-align: center;
padding: 1em 0;
margin: 0 auto;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
& a {
text-decoration: none;
color: rgb(var(--black));
transition: 0.2s ease;
&:has(span) {
border-radius: 5px;
display: inline-block;
text-align: center;
padding: calc(0.5em + 4px) 0.5em 0.5em;
line-height: 1em;
}
&:has(span.secondary-btn) {
border: 1px solid rgba(var(--black), 95%);
}
&:has(span.primary-btn) {
background-color: rgba(var(--black), 95%);
box-shadow: 0 2px 8px rgba(var(--black), 5%);
color: white;
}
&:has(span.primary-btn:hover) {
background-color: var(--accent);
box-shadow: 0 2px 8px var(--accent);
}
&:has(span.secondary-btn:hover) {
border-color: var(--accent);
color: var(--accent);
box-shadow: 0 2px 8px var(--accent);
}
}
}
.hero-image {
width: 100%;
}
.hero-image img {
display: block;
margin: 0 auto;
border-radius: 12px;
}
.prose {
width: 650px;
max-width: calc(100% - 2em);
margin: auto;
padding: 0 1em;
color: rgb(var(--gray-dark));
}
.title {
margin-bottom: 1em;
padding: 1em 0;
text-align: center;
line-height: 1;
& .avatar {
width: 32px;
height: 32px;
border-radius: 50%;
display: inline;
/* height: calc(1rem + 6px); */
margin: 0 0.5rem;
margin-bottom: -10px;
}
& a[rel='author'] {
color: rgb(var(--black));
}
& a[rel='author']:hover {
color: var(--accent);
}
}
.date {
margin-bottom: 0.5em;
color: rgb(var(--gray));
}
.last-updated-on {
font-style: italic;
}
@media (max-width: 700px) {
main {
width: 100%;
& .prose {
max-width: calc(100% - 1em);
padding: 0;
}
& .cta-wrapper {
width: 250px;
& a {
font-size: 0.75em;
}
}
}
}
</style>
</head>
<body>
<Header />
<main>
<article>
<div class="hero-image">
{heroImage && <img width={700} height={510} src={heroImage} alt="" />}
</div>
<div class="prose">
<div class="title">
<div class="date">
<FormattedDate date={pubDate} />
{
updatedDate && (
<div class="last-updated-on">
Last updated on <FormattedDate date={updatedDate} />
</div>
)
}
</div>
<h1>{title}</h1>
<address style="display:inline">
By <img
class="avatar"
src="/ayoayco-avatar.jpg"
alt="Ayo Ayco's Avatar"
/>
<a rel="author" href={SITE_AUTHOR_URL}>{SITE_AUTHOR}</a>
</address>
</div>
<slot />
</div>
</article>
<div class="cta-wrapper">
<a href="/">
<span class="primary-btn">Get Cozy!</span>
</a>
<a href={`mailto:${SITE_AUTHOR_EMAIL}`}>
<span class="secondary-btn">Email Us</span>
</a>
</div>
</main>
<Footer />
</body>
</html>

View file

@ -1,69 +0,0 @@
---
import { ArticleData } from "@extractus/article-extractor";
import "./reset.css";
export interface Props {
meta: ArticleData
}
const appTitle = "Cozy 🧸";
const { meta } = Astro.props;
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{appTitle} {meta.title && `| ${meta.title}`}</title>
<meta property="cozy:title" content={meta.title} />
<meta property="cozy:url" content={meta.url} />
<meta property="cozy:description" content={meta.description} />
<meta property="cozy:image" content={meta.image} />
<meta property="cozy:source" content={meta.source} />
<meta property="cozy:author" content={meta.author} />
<meta property="cozy:published" content={meta.published} />
</head>
<body>
<div id="app-wrapper">
<slot />
<div id="main-content">
<div id="post-wrapper">
<slot name="post" />
</div>
<div id="library-wrapper">
<slot name="library" />
</div>
</div>
</div>
</body>
</html>
<style lang="scss">
#app-wrapper {
width: 100%;
max-width: 650px;
margin: 0 auto;
padding: 0.5rem;
}
#main-content {
* {
margin: 1rem 0;
}
#post-wrapper {
padding: 0 1rem;
}
}
</style>
<style is:global lang="scss">
:root {
--system-ui: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
html * {
font-family: var(--system-ui);
}
</style>

17
src/pages/404.astro Normal file
View file

@ -0,0 +1,17 @@
---
import App from '../layouts/App.astro'
import Library from '../components/Library.astro'
import Footer from '../components/Footer.astro'
import AddressBar from '../components/AddressBar.astro'
export const prerender = false
---
<App article={null}>
<AddressBar url="" />
<div slot="post" id="router-outlet">
<h1>404: Not Found</h1>
</div>
<Library slot="library" skipSave />
<Footer slot="footer" />
</App>

24
src/pages/a.astro Normal file
View file

@ -0,0 +1,24 @@
---
/**
* NOTE: this page is a fix for old cached articles w/ cozified links going to an `a` route
*/
import AddressBar from '../components/AddressBar.astro'
import App from '../layouts/App.astro'
import Library from '../components/Library.astro'
import Footer from '../components/Footer.astro'
export const prerender = false
const url = Astro.url.searchParams.get('url')
return Astro.redirect(`/?url=${url}`)
---
<App article={null}>
<AddressBar url={url} />
<div>
Go to the correct <a href={`/?url=${url}`}>Page</a>
</div>
<Library slot="library" skipSave={true} />
<Footer slot="footer" />
</App>

View file

@ -0,0 +1,20 @@
---
import { type CollectionEntry, getCollection, render } from 'astro:content'
import Blog from '../../layouts/Blog.astro'
export async function getStaticPaths() {
const posts = await getCollection('blog')
return posts.map((post) => ({
params: { id: post.id },
props: post,
}))
}
type Props = CollectionEntry<'blog'>
const post = Astro.props
const { Content } = await render(post)
---
<Blog {...post.data}>
<Content />
</Blog>

129
src/pages/blog/index.astro Normal file
View file

@ -0,0 +1,129 @@
---
import BaseHead from '../../components/blog/BaseHead.astro'
import Header from '../../components/blog/Header.astro'
import Footer from '../../components/blog/Footer.astro'
import FormattedDate from '../../components/blog/FormattedDate.astro'
import { getCollection } from 'astro:content'
import { SITE_TITLE, SITE_DESCRIPTION } from '../../consts'
const posts = (await getCollection('blog')).sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
)
---
<!doctype html>
<html lang="en">
<head>
<BaseHead title={SITE_TITLE} description={SITE_DESCRIPTION} />
<style>
main {
width: 700px;
}
ul {
display: flex;
flex-wrap: wrap;
gap: 2rem;
list-style-type: none;
margin: 0;
padding: 0;
}
ul li * {
text-decoration: none;
transition: 0.2s ease;
}
.card {
border: 1px solid rgb(var(--gray));
border-radius: 12px;
width: 100%;
padding: 1em;
margin-bottom: 1rem;
text-align: center;
color: rgb(var(--black));
position: relative;
background-color: white;
& img {
width: 100%;
max-width: 800px;
}
& .title {
font-size: 2.369rem;
margin: 1rem;
color: rgb(var(--black));
line-height: 1;
}
& .description {
margin-bottom: 0;
}
& h4 a::after {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
cursor: pointer;
}
& h4 {
text-decoration: underline !important;
color: var(--accent);
text-decoration-thickness: 2px !important;
& a {
color: rgb(var(--black));
text-decoration: none;
}
&:hover a {
color: var(--accent);
}
}
}
.date {
margin: 0;
color: rgb(var(--gray));
}
@media (max-width: 700px) {
ul {
gap: 0.5em;
}
ul li {
width: 100%;
text-align: center;
}
}
</style>
</head>
<body>
<Header />
<main>
<section>
<ul>
{
posts.map((post) => (
<li class="card">
{/* {
post.data.heroImage
? <img width={700} height={360} src={post.data.heroImage} alt="" />
: <img width={700} height={360} src="/blog-placeholder-4.jpg" alt="" />
} */}
<small class="date">
<FormattedDate date={post.data.pubDate} />
</small>
<h4 class="title">
<a href={`/blog/${post.id}/`}>{post.data.title}</a>
</h4>
<p class="description">{post.data.description}</p>
</li>
))
}
</ul>
</section>
</main>
<Footer />
</body>
</html>

View file

@ -1,42 +1,74 @@
---
import { ArticleData, extract } from "@extractus/article-extractor";
import AddressBar from "../components/AddressBar.astro";
import Post from "../components/Post.astro";
import Layout from "../layouts/Layout.astro";
import Library from "../components/Library.astro";
import { createClient, type RedisJSON } from 'redis'
import { type ArticleData, extract } from '@extractus/article-extractor'
const params = Astro.url.searchParams;
const url = params.get('url') || '';
let article: ArticleData | null;
let skipSave;
import AddressBar from '../components/AddressBar.astro'
import Post from '../components/Post.astro'
import App from '../layouts/App.astro'
import Library from '../components/Library.astro'
import Footer from '../components/Footer.astro'
const error = {
title: "Something is not right",
content: "<p>The article extractor did not get any result.</p>",
}
// Initialize Redis client
const client = createClient()
client.on('error', (err) => console.error('Redis Client Error', err))
await client.connect()
try {
article = await extract(url);
if (!article ) {
article = error;
skipSave = true;
// Disable prerendering for dynamic content
export const prerender = false
// Get URL parameter from query string
let url = Astro.url.searchParams.get('url')
let article: ArticleData | null = { url: '/' }
// Handle redirect loops by extracting URL from nested parameters
while (url?.startsWith(Astro.url.origin)) {
try {
// Parse the URL to extract search parameters
const parsedUrl = new URL(url)
url = parsedUrl.searchParams.get('url')
} catch {
// If URL parsing fails, break the loop
console.error('Failed to parse URL:', url)
break
}
} catch {
article = error;
skipSave = true;
}
if (url === '') {
article = {
title: "Welcome to Cozy 🧸",
content: "<p>Enter a URL above to get started.</p>",
url: '/'
};
}
// Process article extraction only if a valid URL is provided
if (url && url !== '/' && url !== '') {
const cacheKey = 'cozy:url:' + url
try {
// Check if article exists in Redis cache
const exists = await client.exists(cacheKey)
if (exists) {
// Retrieve cached article data
article = (await client.json.get(cacheKey)) as ArticleData
console.log('>>> Using cached content', article.url)
} else {
// Fetch article from the web
article = await extract(url)
console.log('>>> Using fetched content', article?.url)
if (article !== null && article.url) {
// Cache the fetched article in Redis
await client.json.set(cacheKey, '$', article as RedisJSON)
console.log('>>> Added to cache', article.url)
}
}
} catch (error) {
// Log error and continue with null article
console.error('Error processing article:', error)
article = null
}
}
---
<Layout meta={article}>
<AddressBar url={url} />
<Post slot="post" article={article} />
<Library skipSave={skipSave} slot="library" postDivSelector="#post"/>
</Layout>
<App article={article}>
<AddressBar url={url} />
<div slot="post" id="router-outlet">
<Post article={article} />
</div>
<Library slot="library" skipSave={article === null} />
<Footer slot="footer" />
</App>

177
src/styles/blog.css Normal file
View file

@ -0,0 +1,177 @@
/*
The CSS in this style tag is based off of Bear Blog's default CSS.
https://github.com/HermanMartinus/bearblog/blob/297026a877bc2ab2b3bdfbd6b9f7961c350917dd/templates/styles/blog/default.css
License MIT: https://github.com/HermanMartinus/bearblog/blob/master/LICENSE.md
*/
@font-face {
font-family: 'Atkinson';
src: url('/fonts/atkinson-regular.woff') format('woff');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Atkinson';
src: url('/fonts/atkinson-bold.woff') format('woff');
font-weight: 700;
font-style: normal;
font-display: swap;
}
body {
font-family: 'Atkinson', sans-serif;
margin: 0;
padding: 0;
text-align: left;
background: linear-gradient(var(--gray-gradient)) no-repeat;
background-size: 100% 600px;
word-wrap: break-word;
overflow-wrap: break-word;
color: rgb(var(--gray-dark));
font-size: 20px;
line-height: 1.7;
}
main {
width: 700px;
max-width: calc(100% - 2em);
margin: auto;
padding: 1em;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 0 0 0.5rem 0;
color: rgb(var(--black));
line-height: 1.2;
}
h1 {
font-size: 2.441em;
}
h2 {
font-size: 1.753em;
}
h3 {
font-size: 1.563em;
}
h4 {
font-size: 1.35em;
}
h5 {
font-size: 1.15em;
}
strong,
b {
font-weight: 700;
}
a {
color: var(--accent);
}
a:hover {
color: var(--accent);
}
p,
ul,
ol,
table,
pre.astro-code {
margin-bottom: 1em;
}
.prose {
& p,
& ul,
& ol,
& table {
margin-bottom: 1em;
}
}
textarea {
width: 100%;
font-size: 16px;
}
input {
font-size: 16px;
}
table {
width: 100%;
}
img {
max-width: 100%;
height: auto;
border-radius: 8px;
}
code {
padding: 2px 5px;
background-color: rgb(var(--gray-light));
border-radius: 2px;
}
pre {
padding: 1.5em;
border-radius: 8px;
}
pre > code {
all: unset;
}
blockquote {
border-left: 2px solid var(--accent);
padding: 0 0 0 1em;
margin: 0px;
font-size: 1em;
}
hr {
border: none;
border-top: 1px solid rgb(var(--gray-light));
}
@media (max-width: 700px) {
body {
font-size: 18px;
}
main {
padding: 1em;
}
}
.sr-only {
border: 0;
padding: 0;
margin: 0;
position: absolute !important;
height: 1px;
width: 1px;
overflow: hidden;
/* IE6, IE7 - a 0 height clip, off to the bottom right of the visible 1px box */
clip: rect(1px 1px 1px 1px);
/* maybe deprecated but we need to support legacy browsers */
clip: rect(1px, 1px, 1px, 1px);
/* modern browsers, clip-path works inwards from each corner */
clip-path: inset(50%);
/* added line to stop words getting smushed together (as they go onto separate lines and some screen readers do not understand line feeds as a space */
white-space: nowrap;
}

13
src/styles/variables.css Normal file
View file

@ -0,0 +1,13 @@
:root {
--accent: #3054bf;
--svg-filter-accent: invert(25%) sepia(86%) saturate(1533%) hue-rotate(210deg)
brightness(91%) contrast(90%);
--accent-dark: #203880;
--black: 15, 18, 25;
--gray: 96, 115, 159;
--gray-light: 229, 233, 240;
--gray-dark: 34, 41, 57;
--gray-gradient: rgba(var(--gray-light), 50%), #fff;
--box-shadow: 0 2px 6px rgba(var(--gray), 25%),
0 8px 24px rgba(var(--gray), 33%), 0 16px 32px rgba(var(--gray), 33%);
}

148
src/sw.mjs Normal file
View file

@ -0,0 +1,148 @@
/**
* Note: @ayco/astro-sw integration injects variables `__prefix`, `__version`, & `__assets`
* -- find usage in `astro.config.mjs` integrations
* @see https://ayco.io/n/@ayco/astro-sw
*/
const cacheName = `${__prefix ?? 'app'}-v${__version ?? '000'}`
/**
* Cleans up old caches by deleting any cache that is not in the allowed list.
* This helps prevent the service worker from accumulating unnecessary cached data over time.
* @async
* @function cleanOldCaches
* @returns {Promise<void>} Resolves when all old caches have been deleted
*/
const cleanOldCaches = async () => {
const allowCacheNames = ['cozy-reader', cacheName]
const allCaches = await caches.keys()
allCaches.forEach((key) => {
if (!allowCacheNames.includes(key)) {
console.info('Deleting old cache', key)
caches.delete(key)
}
})
}
/**
* Adds resources to the cache with the specified cache name.
* @async
* @function addResourcesToCache
* @param {string[]} resources - An array of resource URLs to be cached.
* @returns {Promise<void>} Resolves when all resources have been added to the cache.
*/
const addResourcesToCache = async (resources) => {
const cache = await caches.open(cacheName)
console.info('adding resources to cache...', resources)
try {
await cache.addAll(resources)
} catch (error) {
console.error(
'failed to add resources to cache; make sure requests exists and that there are no duplicates',
{
resources,
error,
}
)
}
}
/**
* Adds a response to the cache for a given request.
* If a response already exists for the request, it will be replaced.
* @async
* @function putInCache
* @param {Request} request - The request to cache.
* @param {Response} response - The response to cache.
* @returns {Promise<void>} Resolves when the response has been added to the cache.
*/
const putInCache = async (request, response) => {
const cache = await caches.open(cacheName)
if (response.ok) {
console.info('adding one response to cache...', request.url)
// if exists, replace
cache.keys().then((keys) => {
if (keys.includes(request)) {
cache.delete(request)
}
})
cache.put(request, response)
}
}
const cacheAndRevalidate = async ({ request, fallbackUrl }) => {
const cache = await caches.open(cacheName)
// Try get the resource from the cache
const responseFromCache = await cache.match(request)
if (responseFromCache) {
console.info('using cached response...', responseFromCache.url)
// get network response for revalidation of cached assets
fetch(request.clone())
.then((responseFromNetwork) => {
if (responseFromNetwork) {
console.info('fetched updated resource...', responseFromNetwork.url)
putInCache(request, responseFromNetwork.clone())
}
})
.catch((error) => {
console.info('failed to fetch updated resource', error)
})
return responseFromCache
}
try {
// Try to get the resource from the network for 5 seconds
const responseFromNetwork = await fetch(request.clone())
// response may be used only once
// we need to save clone to put one copy in cache
// and serve second one
putInCache(request, responseFromNetwork.clone())
console.info('using network response', responseFromNetwork.url)
return responseFromNetwork
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
// Try the fallback
const fallbackResponse = await cache.match(fallbackUrl)
if (fallbackResponse) {
console.info('using fallback cached response...', fallbackResponse.url)
return fallbackResponse
}
// when even the fallback response is not available,
// there is nothing we can do, but we must always
// return a Response object
return new Response('Network error happened', {
status: 408,
headers: { 'Content-Type': 'text/plain' },
})
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
self.addEventListener('activate', (event) => {
console.info('activating service worker...')
cleanOldCaches()
})
self.addEventListener('install', (event) => {
console.info('installing service worker...')
self.skipWaiting() // go straight to activate
event.waitUntil(addResourcesToCache(__assets ?? []))
})
self.addEventListener('fetch', (event) => {
console.info('fetch happened', { data: event })
event.respondWith(
cacheAndRevalidate({
request: event.request,
fallbackUrl: './',
})
)
})

View file

@ -1,23 +1,5 @@
export function getPostCard(html: HTMLHtmlElement) {
const title =
html
.querySelector('meta[property="cozy:title"]')
?.getAttribute("content") ||
html.querySelector("title")?.innerHTML?.replace("Cozy 🧸 | ", "");
const description = html
.querySelector('meta[property="cozy:description"]')
?.getAttribute("content");
const image = html
.querySelector('meta[property="cozy:image"]')
?.getAttribute("content");
const source = html
.querySelector('meta[property="cozy:source"]')
?.getAttribute("content");
const published = html
.querySelector('meta[property="cozy:published"]')
?.getAttribute("content");
const { title, description, image, source, published } = getPostMeta(html)
const postCard = `
<div class="post-card">
<div class="post-card__image">
@ -35,32 +17,109 @@ export function getPostCard(html: HTMLHtmlElement) {
? `
<div class="post-card__meta">
${
source
&& `
source &&
`
<p class="post-card__source">${source}</p>
`
}
${
published
&& `
published &&
`
<p class="post-card__published">${
new Date(published)?.toLocaleDateString() || ""
new Date(published)?.toLocaleDateString() || ''
}</p>
`
}
</div>
`
: ""
: ''
}
<h3 class="post-card__title">${title}</h3>
${
description
? `
<p class="post-card__description">${description}</p>`
: ""
: ''
}
</div>
</div>
`;
return postCard;
`
return postCard
}
export function renderPost(
responseText: string | null,
url,
postDivSelector: string,
preventPushState = false
) {
const postDiv = document.querySelector<HTMLDivElement>(`#${postDivSelector}`)
let postText = ''
let cozyUrl = '/'
let cozyTitle = 'Cozy'
if (responseText) {
const html = document.createElement('html')
html.innerHTML = responseText
const newPost = html.querySelector('body')?.querySelector('#post')
postText = newPost?.outerHTML || ''
cozyUrl =
html
.querySelector('meta[property="cozy:url"]')
?.getAttribute('content') ?? '/'
cozyTitle = `${getCozyTitle(html)} | Cozy`
}
if (postDiv) {
postDiv.innerHTML = postText
const appUrl = document.getElementById('app-url') as HTMLInputElement
const backBtn = document.querySelector<HTMLButtonElement>('#app-back')
const submitBtn = document.querySelector<HTMLButtonElement>('#submit')
if (cozyUrl !== '/') {
appUrl.value = cozyUrl || ''
backBtn?.removeAttribute('disabled')
submitBtn?.removeAttribute('disabled')
document.title = cozyTitle
} else {
appUrl.value = ''
backBtn?.setAttribute('disabled', 'true')
submitBtn?.setAttribute('disabled', 'true')
document.title = `Cozy`
}
if (!preventPushState) {
window.history.pushState({ url }, '', url)
}
}
}
function getPostMeta(html: HTMLHtmlElement) {
const title = getCozyTitle(html)
const description = html
.querySelector('meta[property="cozy:description"]')
?.getAttribute('content')
const image = html
.querySelector('meta[property="cozy:image"]')
?.getAttribute('content')
const source = html
.querySelector('meta[property="cozy:source"]')
?.getAttribute('content')
const published = html
.querySelector('meta[property="cozy:published"]')
?.getAttribute('content')
return { title, description, image, source, published }
}
function getCozyTitle(html: HTMLHtmlElement): string | undefined {
return (
html
.querySelector('meta[property="cozy:title"]')
?.getAttribute('content') ??
/**
* backwards compatibility for stuff before we implemented cozy:meta tags
* REMOVE ON V1 release
*/
html.querySelector('title')?.innerHTML?.replace('Cozy 🧸 | ', '')
)
}

24
src/utils/sanitizer.ts Normal file
View file

@ -0,0 +1,24 @@
import { parse, render, transform, walkSync } from 'ultrahtml'
import sanitize from 'ultrahtml/transformers/sanitize'
export async function cozify(html: string, baseUrl: string): Promise<string> {
// remove target="_blank" from links
const ast = parse(html)
walkSync(ast, (node) => {
if (node.name === 'a') {
node.attributes.href = `${baseUrl}?url=${node.attributes.href}`
node.attributes.prefetch = true
}
})
const newHtml = await render(ast)
return transform(newHtml, [
sanitize({
dropElements: ['script'],
dropAttributes: {
target: ['a'],
},
}),
])
}

31
test/sanitizer.test.ts Normal file
View file

@ -0,0 +1,31 @@
import { describe, expect, test } from 'vitest'
import { cozify } from '../src/utils/sanitizer'
describe('cozify()', async () => {
const baseUrl = 'https://cozy.pub'
test('should remove scripts', async () => {
const html = '<h1>HELLO</h1><script>console.log()</script>'
const result = await cozify(html, baseUrl)
expect(result).not.toContain('<script>')
})
test('should remove target=_blank from links', async () => {
const html = "<a href=# target='_blank'>hey</a>"
const result = await cozify(html, baseUrl)
expect(result).not.toContain('target')
console.log(result)
})
test('should add base url to href of links', async () => {
const html = '<a href="#">hey</a>'
const result = await cozify(html, baseUrl)
expect(result).toContain('href="https://cozy.pub?url=#"')
})
test('should add prefetch=true to links', async () => {
const html = '<a href=#>hey</a>'
const result = await cozify(html, baseUrl)
expect(result).toContain('prefetch="true"')
})
})