Compare commits

..

No commits in common. "main" and "v0.3.2" have entirely different histories.
main ... v0.3.2

78 changed files with 4200 additions and 7085 deletions

View file

@ -1,11 +0,0 @@
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}"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

View file

@ -1,5 +0,0 @@
# 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).

3
.gitignore vendored
View file

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

View file

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

View file

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

View file

@ -1 +0,0 @@
{}

View file

@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
Examples of behavior that contributes to a positive environment for our Examples of behavior that contributes to a positive environment for our
community include: community include:
- Demonstrating empathy and kindness toward other people * Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences * Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback * Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes, * Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience 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 community
Examples of unacceptable behavior include: 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 any kind
- Trolling, insulting or derogatory comments, and personal or political attacks * Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment * Public or private harassment
- Publishing others' private information, such as a physical or email address, * Publishing others' private information, such as a physical or email address,
without their explicit permission 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 professional setting
## Enforcement Responsibilities ## Enforcement Responsibilities

View file

@ -7,12 +7,9 @@ Thank you for your interest in building the coziest web experience for people li
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. 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 This project is built with the following tech
1. TypeScript, HTML, SCSS - even just knowing basic JS and CSS will give you familiarity of the code
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. [Astro](https://astro.build) - Astro is the chosen framework, please read throught the basics on their docs if you are unfamiliar
- 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.
- 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 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 ## Setting up the project

View file

@ -1,51 +0,0 @@
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,77 +1,59 @@
<p align="center"> > [!IMPORTANT]
<img src="https://github.com/user-attachments/assets/e49b56a7-cc0f-45a3-98e0-8bbcbd02a47c" alt="COZY logo" /><br /> > ...aaaaaand we're back: [cozy.ayco.io](https://cozy.ayco.io)
Remove distractions. Save for later.<br /> > -ayo
Cozy is your modern-day reading companion.
</p> <h1 align="center">Cozy</h1>
<p align="center"> <p align="center">
<a href="https://github.com/ayoayco/cozy"> <a href="https://github.com/ayoayco/cozy">
<img alt="Last Commit" src="https://img.shields.io/github/last-commit/ayoayco/cozy?logo=github" /> <img alt="Last Commit" src="https://img.shields.io/github/last-commit/ayoayco/cozy?logo=github" />
</a> </a>
<a href="https://github.com/ayoayco/cozy-reader/releases/latest"> <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 />
<img alt="Alpha" src="https://img.shields.io/github/package-json/v/ayoayco/cozy?label=alpha" /> Remove distractions. Save for later.<br />
</a><br /> Cozy is your modern-day reading assistant.<br /><br />
<img src="https://raw.githubusercontent.com/ayoayco/cozy/main/.github/assets/screenshot.png" /><br />
</p> </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? ## How is this app different?
Here's what this project is building: Here's what this project is building:
1. An app that just works, no sign-ups or set-ups. 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 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 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 4. Cloud-sync will be opt-in, with your choice of provider
5. Will also explore smart insights, such as: 5. Will also explore smart insights, such as:
1. text summarization 1. text summarization
2. dominant emotion 2. dominant emotion
3. other sources for lateral reading 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 ## Roadmap
| Feature | Status | Version |
| Feature | Status | Version | | --- | --- | --- |
| ---------------------- | -------------- | ------- | | Remove distractions| ✅ DONE | v0.0.x |
| Remove distractions | ✅ DONE | v0.0.x | | Save viewed history | ✅ DONE | v0.1.x |
| Save viewed history | ✅ DONE | v0.1.x | | Open links within Cozy | ✅ DONE | v0.2.x |
| Open links within Cozy | ✅ DONE | v0.2.x | | Offline access | ✅ DONE | v0.3.x |
| Offline access | ✅ DONE | v0.3.x | | Set items as Favorites | 🛠️ In-progress | v0.4.x |
| Set items as Favorites | 🛠️ In-progress | v0.4.x | | Smart insights | | v0.5.x |
| Smart insights | | v0.5.x | | Browser Extensions | | |
| Browser Extensions | | | | Native Apps | | |
| Native Apps | | |
## Usage / Options ## Usage / Options
**1. Copy, Paste** **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 app's 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** **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.pub/?url=%27 + window.location.href, %27_self%27); })(); javascript:(function(){ window.open('https://cozy.ayco.io/?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: 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) | | ![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...** **3. One-click Extension then...**
@ -79,19 +61,18 @@ This is possible on all major browsers, including Safari on iOS (where I persona
It doesn't exist yet... I'll probably get to creating a browser extension at some point. But for now, PRs welcome! :) It doesn't exist yet... I'll probably get to creating a browser extension at some point. But for now, PRs welcome! :)
## Credits ## Credits
We are thankful for all the building blocks provided by the following projects: 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. [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 communicating necessary data from server to client
1. [@ayco/astro-resume](https://ayco.io/n/@ayco/astro-resume) for serializing server data to be used in the client 1. [@ayco/astro-sw](https://ayco.io/n/@ayco/astro-sw) for taking app's service worker, and injecting needed dynamic assets & variables (simple, and dev's retain control)
1. [@extractus/article-extractor](https://www.npmjs.com/package/@extractus/article-extractor) for the amazing scraping of articles 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. [astro-iconify](https://www.npmjs.com/package/astro-iconify) for easily icon-sets in [iconify](https://icon-sets.iconify.design/)
1. [sass](https://www.npmjs.com/package/sass) for some nested styling
1. [ultrahtml](https://www.npmjs.com/package/ultrahtml) for any cleanup and transformation we do on the received article content 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 1. [fastify](https://fastify.dev/) for our production server and [nginx](https://nginx.org/) as reverse proxy
## Contributing ## Contributing
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. 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! Join our [Discord](https://discord.gg/kkvW7GYNAp) if you need help!

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View file

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

Before

Width:  |  Height:  |  Size: 4.5 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

View file

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

Before

Width:  |  Height:  |  Size: 2.7 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7 KiB

View file

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

View file

@ -1,53 +0,0 @@
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,
},
},
},
]

View file

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

23
index.html Normal file
View file

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cozy 2.0</title>
<style>
html,
body {
background: black;
color: white;
& a {
color: orange
}
}
</style>
</head>
<body>
<h1>Cozy!</h1>
<a href="https://ayco.io/gh/cozy">Coming <em>back</em> sooon-ish!</a>
</body>
</html>

View file

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

Before

Width:  |  Height:  |  Size: 4.5 KiB

View file

@ -1,6 +1,6 @@
{ {
"name": "@ayco/cozy", "name": "@ayco/cozy",
"version": "0.3.3", "version": "0.3.2",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/ayoayco/cozy" "url": "https://github.com/ayoayco/cozy"
@ -8,66 +8,31 @@
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
"homepage": "https://cozy.pub", "homepage": "https://ayco.io/gh/cozy",
"scripts": { "scripts": {
"start": "astro dev", "start": "astro dev",
"dev": "astro dev", "build": "astro build",
"build": "astro telemetry disable && astro build", "build:preview": "astro build && node ./server.mjs",
"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:patch": "npm version patch && npm publish --access public",
"publish:minor": "npm version minor && 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/", "deploy:client": "astro 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/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": { "devDependencies": {
"@astrojs/check": "^0.9.4", "@astrojs/check": "^0.9.2",
"@eslint/compat": "^1.3.1", "@astrojs/mdx": "^3.1.3",
"@eslint/js": "^9.32.0", "@astrojs/node": "^8.3.3",
"@extractus/article-extractor": "^8.0.19", "@astrojs/rss": "^4.0.7",
"astro-eslint-parser": "^1.2.2", "@astrojs/sitemap": "^3.1.6",
"esbuild": "^0.25.8", "@ayco/astro-resume": "^0.4.2",
"eslint": "^9.32.0", "@ayco/astro-sw": "^0.5.1",
"eslint-config-prettier": "^10.1.8", "@extractus/article-extractor": "^8.0.10",
"eslint-plugin-astro": "^1.3.1", "@fastify/middie": "^8.3.1",
"eslint-plugin-jsx-a11y": "^6.10.2", "@fastify/static": "^7.0.4",
"eslint-plugin-prettier": "^5.5.4", "astro": "^4.14.2",
"globals": "^16.3.0", "astro-iconify": "^1.2.0",
"husky": "^9.1.7", "fastify": "^4.28.1",
"lint-staged": "^16.1.4", "sass": "^1.77.8",
"prettier": "^3.6.2", "typescript": "^5.5.4",
"prettier-plugin-astro": "^0.14.1", "ultrahtml": "^1.5.3"
"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"
]
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -1,21 +0,0 @@
/**
* @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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

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

Before

Width:  |  Height:  |  Size: 2.7 KiB

View file

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

Before

Width:  |  Height:  |  Size: 4.5 KiB

View file

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

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View file

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

View file

@ -1,41 +1,18 @@
#!/usr/bin/env node #!/usr/bin/env node
import Fastify from 'fastify' import Fastify from 'fastify';
import fastifyMiddie from '@fastify/middie' import fastifyMiddie from '@fastify/middie';
import fastifyStatic from '@fastify/static' import fastifyStatic from '@fastify/static';
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url';
import { handler as ssrHandler } from './dist/server/entry.mjs' import { handler as ssrHandler } from './dist/server/entry.mjs';
const app = Fastify({ logger: true }) const app = Fastify({ logger: true });
await app await app
.register(import('@fastify/rate-limit'), {
global: true,
max: 25,
timeWindow: 1000 * 60 * 5,
})
.register(fastifyStatic, { .register(fastifyStatic, {
root: fileURLToPath(new URL('./dist/client', import.meta.url)), root: fileURLToPath(new URL('./dist/client', import.meta.url)),
}) })
.register(fastifyMiddie) .register(fastifyMiddie);
app.use(ssrHandler);
app.use(ssrHandler) app.listen({ port: 4321 });
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,30 +1,38 @@
--- ---
import Icon from 'astro-iconify' import Icon from "astro-iconify";
import SettingsPopover from "./SettingsPopover.astro";
export interface Props { export interface Props {
url: string | null url: string | null;
} }
const placeholder = 'Type the article URL here' const placeholder = "Type the article URL here";
const { url } = Astro.props const { url } = Astro.props;
--- ---
<div id="address-bar"> <div class="address-bar">
<form> <form>
<label for="app-url"> <button
<Icon name="ic:round-arrow-forward-ios" /> aria-label="Back"
</label> class="btn left-buttons"
type="button"
id="app-back"
name="app-back"
onclick="history.go(-1); return false;"
>
<Icon name="ic:round-arrow-back-ios" />
</button>
<input <input
type="url" type="url"
id="app-url" id="app-url"
name="url" name="url"
value={url ?? ''} value={url ?? ""}
placeholder={placeholder} placeholder={placeholder}
required required
/> />
<button <button
aria-label="Submit" aria-label="Submit"
class="btn right-buttons primary" class="btn right-buttons"
type="submit" type="submit"
id="submit" id="submit"
> >
@ -39,74 +47,89 @@ const { url } = Astro.props
> >
<Icon name="mdi:home" /> <Icon name="mdi:home" />
</a> </a>
<div aria-label="Settings" id="app-settings" class="btn right-buttons">
<input type="checkbox" id="settings-toggle" hidden />
<label for="settings-toggle"><Icon name="mdi:cog" /></label>
</div>
</form> </form>
<SettingsPopover toggle="settings-toggle" />
</div> </div>
<style> <script>
#address-bar { import { deserialize } from "@ayco/astro-resume";
import type { AppConfig } from "../pages/index.astro";
import { renderPost } from "../utils/library";
const backLink = document.querySelector<HTMLButtonElement>("#app-back");
const submitBtn = document.querySelector<HTMLButtonElement>("#submit");
const urlInput = document.querySelector<HTMLInputElement>("#app-url");
const settings = document.querySelector<HTMLDivElement>("#app-settings");
const homeLink = document.querySelector<HTMLAnchorElement>("#app-home");
const { routerOutlet } = deserialize<AppConfig>('app-config');
homeLink?.addEventListener('click', (e) => {
e.preventDefault();
renderPost(null, '/', routerOutlet)
})
// if js is enabled, show the back and settings button
backLink?.setAttribute("style", "display: block");
settings?.setAttribute("style", "display: block");
if (urlInput?.value === "") {
backLink?.setAttribute("disabled", "true");
submitBtn?.setAttribute("disabled", "true");
}
urlInput?.addEventListener("input", (e) => {
const target = e.target as HTMLInputElement;
if (target.value === "") {
submitBtn?.setAttribute("disabled", "true");
} else {
submitBtn?.removeAttribute("disabled");
}
});
</script>
<style lang="scss">
.address-bar {
width: 100%; width: 100%;
position: relative; position: relative;
} }
form:has(input[type='url']:focus) {
border-color: var(--accent);
box-shadow: 0 1px 10px 0px var(--accent);
}
form { form {
width: 100%; width: 100%;
padding: 0.5rem 1rem; border: 0px;
padding: 0.5rem;
text-align: center; text-align: center;
border-radius: 30px; border-radius: 5px;
border: 2px solid rgb(var(--gray)); border: 1px solid #ccc;
background-color: white; background-color: #f5f5f5;
box-shadow: 0 1px 3px 1px rgb(var(--gray-light)); box-shadow: 0 1px 3px 1px #eee;
display: flex; display: flex;
input[type='url']:focus { input[type="url"] {
outline: none;
}
input[type='url'] {
flex: 3; flex: 3;
border: 0px; background-color: white;
border-radius: 30px; border-radius: 5px;
border: 1px solid #eee;
font-size: normal; font-size: normal;
padding: 0.5rem; padding: 0.5rem;
color: rgb(var(--black)); color: #555;
caret-color: var(--accent);
} }
label { #app-back,
display: block; #app-settings {
border: 0px; display: none;
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;
}
}
.btn.primary {
color: var(--accent);
} }
.btn { .btn {
color: rgb(var(--gray));
display: block; display: block;
border: 0px; border: 0px;
height: 100%; height: 100%;
vertical-align: middle; vertical-align: middle;
background-color: transparent; background-color: transparent;
padding: 0.5rem 0; padding: 0.5rem 0;
color: black;
svg { svg {
border: 0px; border: 0px;
@ -128,8 +151,8 @@ const { url } = Astro.props
color: blue !important; color: blue !important;
} }
.btn[disabled='true'] svg { .btn[disabled="true"] svg {
color: rgb(var(--gray-light)) !important; color: #ccc !important;
cursor: default !important; cursor: default !important;
} }
} }

View file

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

View file

@ -1,25 +0,0 @@
---
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,149 +1,135 @@
--- ---
import Serialize from '@ayco/astro-resume' import Serialize from '@ayco/astro-resume';
export interface Props { export interface Props {
skipSave?: boolean skipSave?: boolean
} }
--- ---
<div id="library"> <div id="library">
<span id="heading"></span> <span id="heading"></span>
<ul id="post-list"></ul> <ul id="post-list"></ul>
</div> </div>
<Serialize id="preferences" data={Astro.props} /> <Serialize id="preferences" data={{...Astro.props}} />
<script> <script>
import { deserialize } from '@ayco/astro-resume';
import type { Props } from './Library.astro';
import type { AppConfig } from '../pages/index.astro';
import { getPostCard, renderPost } from '../utils/library' import { getPostCard, renderPost } from '../utils/library'
import { cozify } from '../utils/sanitizer' import { cozify } from '../utils/sanitizer';
import { deserialize } from '@ayco/astro-resume' import { logError, logInfo } from '../utils/logger.mjs';
const cache = await caches.open('cozy-reader') const cache = await caches.open('cozy-reader');
const baseUrl = window.location.origin + '/a' const baseUrl = window.location.origin;
let url = new URL(window.location.href) let url= new URL(window.location.href);
// only cached unencoded url param // only cached unencoded url param
const urlParam = url.searchParams.get('url') const urlParam = url.searchParams.get('url')
if (urlParam) { if (urlParam) {
url = new URL(`${url.origin}/?url=${urlParam}`) url = new URL(`${url.origin}/?url=${urlParam}`);
} }
const { skipSave } = deserialize<Props>('preferences');
const { skipSave } = deserialize('preferences') ?? '' const { routerOutlet } = deserialize<AppConfig>('app-config');
const includesAppURL = urlParam?.includes(baseUrl) ?? false;
const routerOutlet = 'router-outlet'
const includesAppURL = urlParam?.includes(baseUrl) ?? false
try { try {
if ( if (url.href.slice(0, url.href.length - 1) !== baseUrl && !skipSave && !includesAppURL) {
url.href.slice(0, url.href.length - 1) !== baseUrl && logInfo('adding one to cache', {context: 'cozy-reader', data: url})
!skipSave && await cache.add(url);
!includesAppURL
) {
console.info('adding one to cache', {
context: 'cozy-reader',
data: url,
})
await cache.add(url)
} }
} catch (error) { } catch(error) {
console.error('ERR', { context: 'cozy-reader', data: error }) logError('ERR', {context: 'cozy-reader', data: error})
} }
const cachedRequests = (await cache.keys()).filter((request) => { const cachedRequests = (await cache.keys())
const urlObj = new URL(request.url) .filter(request => {
const urlParam = urlObj.searchParams.get('url') const urlObj = new URL(request.url);
const urlParam = urlObj.searchParams.get('url');
return ( return urlObj.search !== ''
urlObj.search !== '' && && !urlParam?.startsWith(baseUrl)
!urlParam?.startsWith(baseUrl) && && urlParam !== ''
urlParam !== '' && && urlParam !== 'null';
urlParam !== 'null' });
)
})
if (cachedRequests?.length && routerOutlet !== null) { if(cachedRequests?.length && routerOutlet !== null) {
const list = document.querySelector('#post-list') const list = document.querySelector('#post-list');
const heading = document.querySelector( const heading = document.querySelector('#library span#heading') as HTMLHeadingElement;
'#library span#heading' heading.innerHTML = 'History';
) as HTMLHeadingElement
heading.innerHTML = 'History'
cachedRequests.reverse().forEach(async (request) => { cachedRequests
const { url } = request .reverse()
const link = document.createElement('a') .forEach(async (request) => {
const {url} = request;
const link = document.createElement('a');
let responseText;
let responseText
const fullResponse = await cache.match(url) 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) => { try {
responseText = data const responseFromNetwork = await fetch(url, {method: 'GET'});
const cleanedResponse = await cozify(responseText, baseUrl) if (responseFromNetwork && url.slice(0, url.length - 1) !== baseUrl && !skipSave && !includesAppURL) {
const html = document.createElement('html') logInfo('updating cached', {context: 'cozy-reader', data: url})
html.innerHTML = cleanedResponse await cache.put(url, responseFromNetwork);
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) } catch(error) {
link.innerHTML = postCard logError('failed to update cached', {context: 'cozy-reader', data: {url, error}})
}
link.href = 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) => { link.onclick = async (e) => {
e.preventDefault() e.preventDefault();
localStorage.setItem('scrollPosition', window.scrollY.toString()) localStorage.setItem('scrollPosition', window.scrollY.toString());
scrollTo(0, 0) scrollTo(0,0);
console.info('using cached response', { logInfo('using cached response', {context: 'cozy-reader', data: url})
context: 'cozy-reader',
data: url,
})
renderPost(cleanedResponse, url, routerOutlet) renderPost(cleanedResponse, url, routerOutlet)
} }
const item = document.createElement('li') const item = document.createElement('li');
item.appendChild(link) item.appendChild(link);
list?.appendChild(item) list?.appendChild(item);
}) });
}) });
window.addEventListener('popstate', async (data) => { window.addEventListener('popstate', async (data) => {
let url = data.state?.url let url = data.state?.url;
let isHome = false let isHome = false;
if (!url) { if (!url) {
url = window.location.href url = window.location.href;
isHome = true isHome = true;
} else { } else {
// replace scrollPosition // replace scrollPosition
localStorage.setItem('scrollPosition', window.scrollY.toString()) localStorage.setItem('scrollPosition', window.scrollY.toString());
} }
const fullResponse = await cache.match(url) const fullResponse = await cache.match(url)
fullResponse?.text().then(async (data) => { fullResponse?.text().then(async (data) => {
const responseText = data const responseText = data;
const cleanedResponse = await cozify(responseText, baseUrl) const cleanedResponse = await cozify(responseText, baseUrl);
console.info('using cached response', { logInfo('using cached response', {context: 'cozy-reader', data: url})
context: 'cozy-reader', renderPost(cleanedResponse, url, routerOutlet, true);
data: url,
})
renderPost(cleanedResponse, url, routerOutlet, true)
if (isHome) { if (isHome) {
const scrollPosition = localStorage.getItem('scrollPosition') const scrollPosition = localStorage.getItem('scrollPosition');
scrollTo(0, scrollPosition ? parseInt(scrollPosition) : 0) scrollTo(0, scrollPosition ? parseInt(scrollPosition) : 0);
} }
}) });
}) });
} }
</script> </script>
<style> <style lang="scss">
#library { #library {
span#heading { span#heading {
color: #555; color: #555;
@ -152,78 +138,70 @@ export interface Props {
} }
} }
#post-list { #post-list {
list-style: none;
padding-left: 0;
display: grid;
gap: 1em;
li { /**
a { `:global` is needed for elements not generated by Astro
- can be improved by CSS in JS, but... this is fine
*/
:global(li) {
list-style: none;
width: calc(100% + 40px);
margin-left: -40px;
:global(a) {
text-decoration: none; text-decoration: none;
color: #000; color: #000;
h3 { :global(h3) {
text-decoration: underline; text-decoration: underline;
} }
.post-card { :global(.post-card) {
display: grid; padding-bottom: 1rem;
grid-template-columns: calc(70px + 0.5em) auto;
.post-card__image { :global(.post-card__image) {
img, float: left;
svg { margin: 0.25rem 0.5rem 0.25rem 0;
:global(img, svg) {
width: 70px; width: 70px;
height: 70px; height: 70px;
object-fit: cover; object-fit: cover;
border-radius: 5px; border-radius: 5px;
border: 2px solid rgb(var(--gray)); border: 1px solid #eee;
background-color: rgb(var(--gray));
margin: 0.15rem 0;
} }
svg { :global(svg) {
color: rgb(var(--gray-light)); color: #ccc;
padding: 0.5em; padding: 0.5rem;
} }
} }
}
: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;
.post-card__content { * {
display: flex; flex: 1;
flex-direction: column;
justify-content: center;
min-height: calc(70px + 0.5rem);
} }
.post-card__title, :global(.post-card__source) {
.post-card__description { font-weight: bold;
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;
}
} }
} }
} }

View file

@ -1,112 +1,98 @@
--- ---
import { ArticleData } from '@extractus/article-extractor' import { ArticleData } from "@extractus/article-extractor";
import { cozify } from '../utils/sanitizer' import { cozify } from "../utils/sanitizer"
export interface Props { export interface Props {
article: ArticleData | null article: ArticleData | null;
} }
const error: ArticleData = { const error: ArticleData = {
title: 'Something is not right', title: 'Something is not right',
content: '<p>The article extractor did not get any information.</p>', content: '<p>The article extractor did not get any information.</p>',
} }
let { article } = Astro.props let { article } = Astro.props;
article ??= error article ??= error;
const datePublished = 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) const cleanContent = await cozify(article.content ?? '', Astro.url.origin)
--- ---
<!-- <!--
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. 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 !== '/' && ( <div id="post">
<article id="post"> {
{article.source && <span class="source">{article.source}</span>} article && article.url !== '/' &&
{article.title && <h1 class="title">{article.title}</h1>} <>
{(article.author || datePublished) && ( {article.source && <span class="source">{article.source}</span>}
<ul class="publish-info"> {article.title && <h1 class="title">{article.title}</h1>}
{article.author && <li>{article.author} </li>} {(article.author || datePublished) && (
{datePublished && <li>{datePublished}</li>} <ul class="publish-info">
</ul> {article.author && <li>{article.author} </li>}
)} {datePublished && <li>{datePublished}</li>}
<content set:html={cleanContent} /> </ul>
</article> )}
) <content set:html={cleanContent} />
} </>
}
</div>
<style> <style is:global lang="scss">
#post {
h1.title { @counter-style publish-icons {
font-size: xx-large; system: cyclic;
margin: 0; symbols: "️✍️" "🗓️";
suffix: " ";
}
h1.title {
font-size: xx-large;
}
h1, h2, h3, h4, h5, h6 {
line-height: 1.2;
}
.source, .publish-info {
font-size: smaller;
color: #555;
}
.source {
font-weight: bold;
}
.publish-info {
margin: 0.3em -1em 1em;
list-style: publish-icons;
}
content {
p, table, ul, img {
margin: 1em 0 !important;
font-size: 20px;
} }
h1, table {
h2, border-collapse: collapse;
h3,
h4, td, th {
h5, border: 1px solid #ccc;
h6 { padding: 0.5em;
line-height: 1.2;
}
.source,
.publish-info {
font-size: smaller;
color: #555;
}
.source {
font-weight: bold;
}
.publish-info {
padding-left: 0;
margin: 0;
list-style: none;
li {
margin: 0;
} }
} }
content { pre {
p, white-space: pre-wrap;
table, &:has(code) {
ul, padding: 1em;
img { background: #f6f8fa;
margin: 1em 0 !important; border-radius: 5px;
font-size: 20px;
} }
}
table {
border-collapse: collapse; @media (max-width: 600px) {
p, table, ul, img {
td, font-size: 16px;
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,91 @@
---
import Serialize from "@ayco/astro-resume";
import Icon from "astro-iconify";
import { featureFlags } from "../utils/feature-flags";
export interface Props {
toggle: string;
}
const enabledSettings = Object.keys(featureFlags).filter(
(key) => featureFlags[key]
);
const { toggle } = Astro.props;
---
<form id="settings-form" hidden>
<div id="toolbar">
<h2>Settings</h2>
<label for={toggle}>
<Icon name="mdi:close" />
</label>
</div>
{
enabledSettings.map(
(settings, index) =>
settings !== "" && (
<div class="field">
<input
type="checkbox"
id={`settings-${index}`}
name={`settings-${index}`}
/>
<label for={`settings-${index}`}>{settings}</label>
</div>
)
)
}
<small>This is where feature flags will be set once made available. <a href="https://github.com/ayoayco/cozy/issues/new" target="_blank">Request features or report bugs here.</a></small>
</form>
<Serialize id="settings-toggle-id" data={{toggle}} />
<script>
/**
* temporary JS solution, should be replaced with CSS
*/
import { deserialize } from "@ayco/astro-resume";
const {toggle} = deserialize("settings-toggle-id");
document
.getElementById(toggle)
?.addEventListener("change", (e) => {
if (e.currentTarget?.["checked"])
document.getElementById("settings-form")?.removeAttribute("hidden");
else document.getElementById("settings-form")?.setAttribute("hidden", "");
});
</script>
<style lang="scss">
#settings-form {
border: 1px solid #ccc;
border-radius: 5px;
background-color: white;
padding: 0.5em;
width: 300px;
position: absolute;
top: 0.5em;
right: 0.1em;
box-shadow: 0 1px 3px 1px #eee;
#toolbar {
display: flex;
margin-bottom: 0.5em;
h2,
svg {
flex: 1;
}
svg {
width: 24px;
height: 24px;
cursor: pointer;
}
}
.field {
margin-left: 0.5em;
margin-bottom: 0.5em;
}
}
</style>

View file

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

View file

@ -1,81 +1,54 @@
--- ---
import { import { SITE_AUTHOR, SITE_AUTHOR_MASTODON, SITE_PROJECT_REPO } from "../../consts";
SITE_AUTHOR, const today = new Date();
SITE_AUTHOR_EMAIL,
SITE_AUTHOR_MASTODON,
SITE_PROJECT_REPO,
} from '../../consts'
const today = new Date()
--- ---
<footer> <footer>
<p>&copy; {today.getFullYear()} {SITE_AUTHOR}. All rights reserved.</p> &copy; {today.getFullYear()} {SITE_AUTHOR}. All rights reserved.
<p> <div class="social-links">
Want to get in touch? Send a mail to <a href={`mailto:${SITE_AUTHOR_EMAIL}`} <a href={SITE_AUTHOR_MASTODON} target="_blank">
>Cozy at ayco.io</a <span class="sr-only">Follow Astro on Mastodon</span>
>. <svg
</p> viewBox="0 0 16 16"
<div class="social-links"> aria-hidden="true"
<a href={SITE_AUTHOR_MASTODON} target="_blank"> width="32"
<span class="sr-only">Follow Ayo on Mastodon</span> height="32"
<svg astro-icon="social/mastodon"
viewBox="0 0 16 16" ><path
aria-hidden="true" fill="currentColor"
width="32" 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"
height="32" ></path></svg
astro-icon="social/mastodon" >
><path </a>
fill="currentColor" <a href={SITE_PROJECT_REPO} target="_blank">
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" <span class="sr-only">Go to Astro's GitHub repo</span>
></path></svg <svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32" astro-icon="social/github"
> ><path
</a> fill="currentColor"
<a href={SITE_PROJECT_REPO} target="_blank"> 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"
<span class="sr-only">Go to Cozy's GitHub repo</span> ></path></svg
<svg >
viewBox="0 0 16 16" </a>
aria-hidden="true" </div>
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> </footer>
<style> <style>
footer { footer {
padding: 2em 1em 6em 1em; padding: 2em 1em 6em 1em;
background: linear-gradient(var(--gray-gradient)) no-repeat; background: linear-gradient(var(--gray-gradient)) no-repeat;
color: rgb(var(--gray)); color: rgb(var(--gray));
text-align: center; text-align: center;
}
& p { .social-links {
margin-bottom: 0; display: flex;
justify-content: center;
& a { gap: 1em;
color: rgb(var(--gray)); margin-top: 1em;
}
&:hover { .social-links a {
color: rgb(var(--gray-dark)); text-decoration: none;
} color: rgb(var(--gray));
} }
} .social-links a: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> </style>

View file

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

View file

@ -1,140 +1,131 @@
--- ---
import { import { SITE_AUTHOR_MASTODON, SITE_DESCRIPTION, SITE_PROJECT_REPO, SITE_TITLE } from '../../consts';
SITE_AUTHOR_MASTODON,
SITE_DESCRIPTION,
SITE_PROJECT_REPO,
SITE_TITLE,
} from '../../consts'
--- ---
<header> <header>
<nav> <nav>
<div class="site-title"> <div class="site-title">
<h2><a href="/blog">{SITE_TITLE}</a></h2> <h2><a href="/blog">{SITE_TITLE}</a></h2>
<small class="site-description">{SITE_DESCRIPTION}</small> <small class="site-description">{SITE_DESCRIPTION}</small>
</div> </div>
<div class="social-links"> <div class="social-links">
<a href="/"> <a href="/">
<span class="primary-btn">Get Cozy!</span> <span class="primary-btn">Get Cozy!</span>
</a> </a>
<a href={SITE_AUTHOR_MASTODON} target="_blank"> <a href={SITE_AUTHOR_MASTODON} target="_blank">
<span class="sr-only">Follow Ayo on Mastodon</span> <span class="sr-only">Follow Ayo on Mastodon</span>
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32" <svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32"
><path ><path
fill="currentColor" 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" 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 ></path></svg
> >
</a> </a>
<a href={SITE_PROJECT_REPO} target="_blank"> <a href={SITE_PROJECT_REPO} target="_blank">
<span class="sr-only">Go to Cozy's GitHub repo</span> <span class="sr-only">Go to Astro's GitHub repo</span>
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32" <svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32"
><path ><path
fill="currentColor" 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" 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 ></path></svg
> >
</a> </a>
</div> </div>
</nav> </nav>
</header> </header>
<style> <style>
header { header {
margin: 0; margin: 0;
padding: 0 1em; padding: 0 1em;
background: white; background: white;
box-shadow: 0 2px 8px rgba(var(--black), 5%); box-shadow: 0 2px 8px rgba(var(--black), 5%);
width: 100%; }
} h2 {
h2 { margin: 0;
margin: 0; font-size: x-large;
font-size: x-large; }
}
h2 a, h2 a,
h2 a.active { h2 a.active {
text-decoration: none; text-decoration: none;
} }
nav { nav {
width: 900px; display: flex;
max-width: 100%; align-items: center;
display: flex; justify-content: space-between;
align-items: center; padding: 1em 0.5em;
justify-content: space-between;
padding: 1em 0.5em;
margin: 0 auto;
& span.primary-btn { & span.primary-btn {
background-color: rgba(var(--black), 95%); background-color: rgba(var(--black), 95%);
box-shadow: 0 2px 8px rgba(var(--black), 5%); box-shadow: 0 2px 8px rgba(var(--black), 5%);
color: white; color: white;
border-radius: 5px; border-radius: 5px;
display: inline-block; display: inline-block;
text-align: center; text-align: center;
padding: 1px 0.5em 0; padding: 0 0.5em;
transition: 0.2s ease; }
}
}
nav .social-links a:hover { }
& span {
background-color: var(--accent);
box-shadow: 0 2px 8px var(--accent);
}
}
nav .site-title { nav .social-links a:hover {
display: flex; & span {
} background-color: var(--accent);
box-shadow: 0 2px 8px var(--accent);
}
}
nav a, nav .site-title {
nav .site-description { display: flex;
color: var(--black); }
border-bottom: 4px solid transparent;
text-decoration: none;
}
nav a { nav a,
padding: 0 0.5em; nav .site-description {
} color: var(--black);
nav a:hover { border-bottom: 4px solid transparent;
color: var(--accent); text-decoration: none;
} }
nav a.active { nav a {
text-decoration: none; padding: 0 0.5em;
border-bottom-color: var(--accent); }
} nav a:hover {
.social-links, color: var(--accent);
.social-links a { }
display: flex;
}
@media (max-width: 700px) { nav a.active {
nav { text-decoration: none;
display: block; border-bottom-color: var(--accent);
padding: 1em 0; }
.social-links,
.social-links a {
display: flex;
}
& .site-description { @media (max-width: 720px) {
font-size: 1rem; nav {
} display: block;
padding: 1em 0;
& span.primary-btn { & .site-description {
line-height: 1.5rem; font-size: 1rem;
height: 1.5rem; }
}
& .social-links a { & span.primary-btn {
font-size: small; line-height: 1.5rem;
height: 1.5rem;
}
svg { & .social-links a {
height: 1.5rem; font-size: small;
}
} svg {
} height: 1.5rem;
nav a { }
padding: 0.5em; }
padding-left: 0; }
} nav a {
} padding: 0.5em;
padding-left: 0;
}
}
</style> </style>

View file

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

View file

@ -1,9 +1,8 @@
export const SITE_TITLE = 'Cozy Blog' export const SITE_TITLE = 'Cozy Blog';
export const SITE_AUTHOR = 'Ayo Ayco' export const SITE_AUTHOR = 'Ayo Ayco';
export const SITE_AUTHOR_URL = 'https://ayo.ayco.io' 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_AUTHOR_MASTODON = 'https://social.ayco.io/@ayo' export const SITE_PROJECT_REPO = 'https://github.com/ayoayco/Cozy';
export const SITE_PROJECT_REPO = 'https://github.com/ayoayco/Cozy' export const SITE_DESCRIPTION = 'The Web is Yours.';
export const SITE_DESCRIPTION = 'The Web is Yours.'
export const VERSION = 'Drooling-Dogs' export const VERSION = 'Quivering-Quacks';

View file

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

@ -13,7 +13,7 @@ Do you sometimes wish you can have a consistent experience when opening articles
Ah, well you're not alone. 🤣 Ah, well you're not alone. 🤣
This is exactly why I started [**Cozy** 🧸](https://cozy.pub/). This is exactly why I started [**Cozy** 🧸](https://cozy.ayco.io/).
It's a simple web page that can make any web page content-focused! 🎉 It's a simple web page that can make any web page content-focused! 🎉
@ -30,7 +30,6 @@ The project and the road map for features are all public on my [GitHub](https://
Right now, it successfully extracts the content and delivers a clean page to your browser. 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: I'm working toward bringing the following in the coming weeks:
1. Save favorites to a library 1. Save favorites to a library
2. Offline access 2. Offline access
3. Smart Insights about the article 3. Smart Insights about the article
@ -45,13 +44,13 @@ Basically you can have a button there beside your other bookmarks that will open
You can create this new bookmark titled 'Get cozy!' and put the following as value for the URL: You can create this new bookmark titled 'Get cozy!' and put the following as value for the URL:
```js ```js
javascript:(function(){ window.open('https://cozy.pub/?url=%27 + window.location.href, %27_self%27); })(); javascript:(function(){ window.open('https://cozy.ayco.io/?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: 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-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) | | ![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! ## Join the Project!
@ -64,4 +63,4 @@ I think there's lots of good a simple tool could bring if it allows users to cut
This project is a groundwork for this experience. This project is a groundwork for this experience.
Let's build the web we want! 🧸 Let's build the web we want! 🧸

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

@ -0,0 +1,16 @@
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content',
// 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

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

@ -1,11 +0,0 @@
---
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 />).

1
src/env.d.ts vendored
View file

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

View file

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

View file

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

148
src/layouts/BlogPost.astro Normal file
View file

@ -0,0 +1,148 @@
---
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 } 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: 100%;
text-align: center;
padding: 1em 0;
}
& 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: 0 0.5em;
}
& span.primary-btn:hover {
background-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: 720px;
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: 720px) {
main {
width: 100%;
.prose {
max-width: calc(100% - 1em);
padding: 0;
}
}
}
</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>
<hr />
<div class="cta-wrapper">
<a href="/">
<span class="primary-btn">Get Cozy!</span>
</a>
</div>
</main>
<Footer />
</body>
</html>

91
src/layouts/Layout.astro Normal file
View file

@ -0,0 +1,91 @@
---
import { ArticleData } from "@extractus/article-extractor";
import "../styles/reset.css";
import Footer from "../components/Footer.astro";
export interface Props {
article: ArticleData | null
}
const { article } = Astro.props;
const appTitle = article?.title ? `${article.title} | Cozy` : 'Cozy';
---
<!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="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="og:title" content={appTitle} />
<meta property="og:url" content={Astro.url.href} />
<meta property="og:description" content="Remove distractions. Save your favorites. Get useful insights. Cozy is your modern-day reading assistant." />
<meta name="description" content="Remove distractions. Save your favorites. Get useful insights. Cozy is your modern-day reading assistant." />
<meta itemprop="description" content="Remove distractions. Save your favorites. Get useful insights. Cozy is your modern-day reading assistant." />
)
}
<link rel="shortcut icon" type="image/png" href="/favicon.ico" />
</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>
<slot name="footer">
</div>
</body>
</html>
<style lang="scss">
#app-wrapper {
width: 100%;
max-width: 650px;
margin: 0 auto;
padding: 0.5rem;
padding-bottom: 2rem;
}
#main-content {
* {
margin: 1rem 0 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>

View file

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

View file

@ -1,24 +0,0 @@
---
/**
* 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

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

View file

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

View file

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

View file

@ -1,74 +1,41 @@
--- ---
import { createClient, type RedisJSON } from 'redis' import Serialize from "@ayco/astro-resume";
import { type ArticleData, extract } from '@extractus/article-extractor' 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 Footer from "../components/Footer.astro";
import AddressBar from '../components/AddressBar.astro' export const prerender = false;
import Post from '../components/Post.astro'
import App from '../layouts/App.astro'
import Library from '../components/Library.astro'
import Footer from '../components/Footer.astro'
// Initialize Redis client const appConfig = {
const client = createClient() routerOutlet: 'router-outlet',
client.on('error', (err) => console.error('Redis Client Error', err)) };
await client.connect() export type AppConfig = typeof appConfig;
// Disable prerendering for dynamic content let url = Astro.url.searchParams.get('url');
export const prerender = false let article: ArticleData | null = {url: '/'};
// 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)) { while (url?.startsWith(Astro.url.origin)) {
url = new URL(url).searchParams.get('url');
}
if (url)
try { try {
// Parse the URL to extract search parameters article = await extract(url);
const parsedUrl = new URL(url)
url = parsedUrl.searchParams.get('url')
} catch { } catch {
// If URL parsing fails, break the loop article = null;
console.error('Failed to parse URL:', url)
break
} }
}
// 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 article={article}>
<AddressBar url={url} />
<div slot="post" id={appConfig.routerOutlet}>
<Post article={article} />
</div>
<Library slot="library" skipSave={article === null} />
<Footer slot="footer" />
</Layout>
<App article={article}> <Serialize id="app-config" data={appConfig} />
<AddressBar url={url} />
<div slot="post" id="router-outlet">
<Post article={article} />
</div>
<Library slot="library" skipSave={article === null} />
<Footer slot="footer" />
</App>

View file

@ -4,41 +4,54 @@
License MIT: https://github.com/HermanMartinus/bearblog/blob/master/LICENSE.md License MIT: https://github.com/HermanMartinus/bearblog/blob/master/LICENSE.md
*/ */
@font-face { :root {
font-family: 'Atkinson'; --accent: #2337ff;
src: url('/fonts/atkinson-regular.woff') format('woff'); --accent-dark: #000d8a;
font-weight: 400; --black: 15, 18, 25;
font-style: normal; --gray: 96, 115, 159;
font-display: swap; --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%);
} }
@font-face { @font-face {
font-family: 'Atkinson'; font-family: 'Atkinson';
src: url('/fonts/atkinson-bold.woff') format('woff'); src: url('/fonts/atkinson-regular.woff') format('woff');
font-weight: 700; font-weight: 400;
font-style: normal; font-style: normal;
font-display: swap; 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 { body {
font-family: 'Atkinson', sans-serif; font-family: 'Atkinson', sans-serif;
margin: 0; margin: 0;
padding: 0; padding: 0;
text-align: left; text-align: left;
background: linear-gradient(var(--gray-gradient)) no-repeat; background: linear-gradient(var(--gray-gradient)) no-repeat;
background-size: 100% 600px; background-size: 100% 600px;
word-wrap: break-word; word-wrap: break-word;
overflow-wrap: break-word; overflow-wrap: break-word;
color: rgb(var(--gray-dark)); color: rgb(var(--gray-dark));
font-size: 20px; font-size: 20px;
line-height: 1.7; line-height: 1.7;
} }
main { main {
width: 700px; width: 700px;
max-width: calc(100% - 2em); max-width: calc(100% - 2em);
margin: auto; margin: auto;
padding: 1em; padding: 1em;
} }
h1, h1,
@ -47,42 +60,42 @@ h3,
h4, h4,
h5, h5,
h6 { h6 {
margin: 0 0 0.5rem 0; margin: 0 0 0.5rem 0;
color: rgb(var(--black)); color: rgb(var(--black));
line-height: 1.2; line-height: 1.2;
} }
h1 { h1 {
font-size: 2.441em; font-size: 2.441em;
} }
h2 { h2 {
font-size: 1.753em; font-size: 1.953em;
} }
h3 { h3 {
font-size: 1.563em; font-size: 1.563em;
} }
h4 { h4 {
font-size: 1.35em; font-size: 1.25em;
} }
h5 { h5 {
font-size: 1.15em; font-size: 1.15em;
} }
strong, strong,
b { b {
font-weight: 700; font-weight: 700;
} }
a { a {
color: var(--accent); color: var(--accent);
} }
a:hover { a:hover {
color: var(--accent); color: var(--accent);
} }
p, p,
@ -90,88 +103,89 @@ ul,
ol, ol,
table, table,
pre.astro-code { pre.astro-code {
margin-bottom: 1em; margin-bottom: 1em;
} }
.prose { .prose {
& p,
& ul, & p,
& ol, & ul,
& table { & ol,
margin-bottom: 1em; & table {
} margin-bottom: 1em;
}
} }
textarea { textarea {
width: 100%; width: 100%;
font-size: 16px; font-size: 16px;
} }
input { input {
font-size: 16px; font-size: 16px;
} }
table { table {
width: 100%; width: 100%;
} }
img { img {
max-width: 100%; max-width: 100%;
height: auto; height: auto;
border-radius: 8px; border-radius: 8px;
} }
code { code {
padding: 2px 5px; padding: 2px 5px;
background-color: rgb(var(--gray-light)); background-color: rgb(var(--gray-light));
border-radius: 2px; border-radius: 2px;
} }
pre { pre {
padding: 1.5em; padding: 1.5em;
border-radius: 8px; border-radius: 8px;
} }
pre > code { pre>code {
all: unset; all: unset;
} }
blockquote { blockquote {
border-left: 2px solid var(--accent); border-left: 2px solid var(--accent);
padding: 0 0 0 1em; padding: 0 0 0 1em;
margin: 0px; margin: 0px;
font-size: 1em; font-size: 1em;
} }
hr { hr {
border: none; border: none;
border-top: 1px solid rgb(var(--gray-light)); border-top: 1px solid rgb(var(--gray-light));
} }
@media (max-width: 700px) { @media (max-width: 720px) {
body { body {
font-size: 18px; font-size: 18px;
} }
main { main {
padding: 1em; padding: 1em;
} }
} }
.sr-only { .sr-only {
border: 0; border: 0;
padding: 0; padding: 0;
margin: 0; margin: 0;
position: absolute !important; position: absolute !important;
height: 1px; height: 1px;
width: 1px; width: 1px;
overflow: hidden; overflow: hidden;
/* IE6, IE7 - a 0 height clip, off to the bottom right of the visible 1px box */ /* IE6, IE7 - a 0 height clip, off to the bottom right of the visible 1px box */
clip: rect(1px 1px 1px 1px); clip: rect(1px 1px 1px 1px);
/* maybe deprecated but we need to support legacy browsers */ /* maybe deprecated but we need to support legacy browsers */
clip: rect(1px, 1px, 1px, 1px); clip: rect(1px, 1px, 1px, 1px);
/* modern browsers, clip-path works inwards from each corner */ /* modern browsers, clip-path works inwards from each corner */
clip-path: inset(50%); 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 */ /* 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; white-space: nowrap;
} }

View file

@ -1,13 +0,0 @@
: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%);
}

145
src/sw.js Normal file
View file

@ -0,0 +1,145 @@
/**
* 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'}`
const forceLogging = false;
/**
* TODO: remove this once astro-sw allows importing utils
*/
function logInfo(message, {context, force, data} = {}) {
context = context !== ''
? `[${context}]: `
: ''
if (force) {
console.info(`${context}${message}`, data ?? '');
}
}
const addResourcesToCache = async (resources) => {
const cache = await caches.open(cacheName);
logInfo('adding resources to cache...', { force: forceLogging, context: 'cozy-sw', data: resources })
await cache.addAll(resources);
};
const putInCache = async (request, response) => {
const cache = await caches.open(cacheName);
logInfo('adding one response to cache...', { force: forceLogging, context: 'cozy-sw', data: request.url })
// if exists, replace
const keys = await cache.keys();
if (keys.includes(request)) {
cache.delete(request);
}
await cache.put(request, response);
};
const cacheAndRevalidate = async ({ request, preloadResponsePromise, fallbackUrl }) => {
const cache = await caches.open(cacheName);
// Try get the resource from the cache
const responseFromCache = await cache.match(request);
try {
// get network response for revalidation of stale assets
const responseFromNetwork = await fetch(request.clone());
if (responseFromNetwork) {
logInfo('updated cached resource...', { force: forceLogging, context: 'cozy-sw', data: responseFromNetwork.url })
putInCache(request, responseFromNetwork.clone());
}
if (responseFromCache) {
logInfo('using cached response...', { force: forceLogging, context: 'cozy-sw', data: responseFromCache.url })
return responseFromCache;
} else {
logInfo('using network response...', { force: forceLogging, context: 'cozy-sw', data: responseFromNetwork.url })
return responseFromNetwork;
}
} catch (error) {
logInfo('failed to fetch updated resource', { force: forceLogging, context: 'cozy-sw', data: request.url })
if (responseFromCache) {
logInfo('using cached response', { force: forceLogging, context: 'cozy-sw', data: responseFromCache.url })
return responseFromCache;
}
}
// Try to use the preloaded response, if it's there
// NOTE: Chrome throws errors regarding preloadResponse, see:
// https://bugs.chromium.org/p/chromium/issues/detail?id=1420515
// https://github.com/mdn/dom-examples/issues/145
// To avoid those errors, remove or comment out this block of preloadResponse
// code along with enableNavigationPreload() and the "activate" listener.
const preloadResponse = await preloadResponsePromise;
if (preloadResponse) {
putInCache(request, preloadResponse.clone());
logInfo('using preload response', { force: forceLogging, context: 'cozy-sw', data: preloadResponse.url })
return preloadResponse;
}
try {
// Try to get the resource from the network for 5 seconds
const responseFromNetwork = await fetch(request.clone(), { signal: AbortSignal.timeout(5000) });
// 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());
logInfo('using network response', { force: forceLogging, context: 'cozy-sw', data: responseFromNetwork.url })
return responseFromNetwork;
} catch (error) {
// Try the fallback
const fallbackResponse = await cache.match(fallbackUrl);
if (fallbackResponse) {
logInfo('using fallback cached response...', { force: forceLogging, context: 'cozy-sw', data: 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' },
});
}
};
const enableNavigationPreload = async () => {
if (self.registration.navigationPreload) {
// Enable navigation preloads!
await self.registration.navigationPreload.enable();
}
};
self.addEventListener('activate', (event) => {
logInfo('activating service worker...', { force: forceLogging, context: 'cozy-sw' })
event.waitUntil(enableNavigationPreload());
});
self.addEventListener('install', (event) => {
logInfo('installing service worker...', { force: forceLogging, context: 'cozy-sw' })
event.waitUntil(
addResourcesToCache([
...(__assets ?? [])
])
);
self.skipWaiting(); // activate updated SW
});
self.addEventListener('fetch', (event) => {
// ... else, use network first
event.respondWith(
cacheAndRevalidate({
request: event.request,
preloadResponsePromise: event.preloadResponse,
fallbackUrl: './',
})
);
});

View file

@ -1,148 +0,0 @@
/**
* 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

@ -0,0 +1,6 @@
export type Feature = 'Send to Email' | 'Hide Images';
export const featureFlags: Record<Feature, boolean> = {
'Send to Email': false,
'Hide Images': false,
};

View file

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

54
src/utils/logger.mjs Normal file
View file

@ -0,0 +1,54 @@
// @ts-check
const isDev = import.meta.env.DEV;
/**
* @typedef {{
* force?: true
* context?: string,
* data?: any
* }} LogOptions
*/
/**
* @param {string} message
* @param {LogOptions} options
*/
export function logMessage(message, {context, force, data} = {}) {
context = context !== ''
? `[${context}]: `
: ''
if (force || isDev) {
console.log(`${context}${message}`, data ?? '');
}
}
/**
* @param {string} message
* @param {LogOptions} options
*/
export function logInfo(message, {context, force, data} = {}) {
context = context !== ''
? `[${context}]: `
: ''
if (force || isDev) {
console.info(`${context}${message}`, data ?? '');
}
}
/**
* @param {string} message
* @param {LogOptions} options
*/
export function logError(message, {context, force, data} = {}) {
context = context !== ''
? `[${context}]: `
: ''
if (force || isDev) {
console.error(`${context}${message}`, data ?? '');
}
}

View file

@ -2,23 +2,28 @@ import { parse, render, transform, walkSync } from 'ultrahtml'
import sanitize from 'ultrahtml/transformers/sanitize' import sanitize from 'ultrahtml/transformers/sanitize'
export async function cozify(html: string, baseUrl: string): Promise<string> { 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) // 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
}
})
return transform(newHtml, [ const newHtml = await render(ast);
sanitize({
dropElements: ['script'], return transform(newHtml, [
dropAttributes: { sanitize({
target: ['a'], dropElements: ['script'],
}, dropAttributes: {
}), target: ['a']
]) }
})
])
} }
function set(value: string) {
return () => value
}

View file

@ -1,31 +0,0 @@
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"')
})
})