Compare commits
No commits in common. "main" and "v0.1.27" have entirely different histories.
11
.build.yml
|
@ -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}"
|
|
BIN
.github/assets/screen-2.png
vendored
Before Width: | Height: | Size: 64 KiB |
BIN
.github/assets/screenshot.png
vendored
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 45 KiB |
5
.github/pull_request_template.md
vendored
|
@ -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).
|
|
6
.gitignore
vendored
|
@ -1,11 +1,7 @@
|
||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
.astro/
|
.netlify/
|
||||||
.output/
|
|
||||||
.continue/
|
|
||||||
package-lock.json
|
|
||||||
|
|
||||||
*~
|
*~
|
||||||
*swo
|
*swo
|
||||||
*swp
|
*swp
|
||||||
.eslintcache
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
npx lint-staged
|
|
|
@ -1,7 +0,0 @@
|
||||||
# someday let's think about formatting html
|
|
||||||
**/*.html
|
|
||||||
|
|
||||||
**/*.md
|
|
||||||
**/*.css
|
|
||||||
**/*.yml
|
|
||||||
**/*.yaml
|
|
1
.vscode/settings.json
vendored
|
@ -1 +0,0 @@
|
||||||
{}
|
|
|
@ -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
|
||||||
|
@ -59,7 +59,8 @@ representative at an online or offline event.
|
||||||
## Enforcement
|
## Enforcement
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
reported via email to the community leaders responsible for enforcement at ayo.ayco@pm.me
|
reported to the community leaders responsible for enforcement at
|
||||||
|
[INSERT CONTACT METHOD].
|
||||||
All complaints will be reviewed and investigated promptly and fairly.
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
All community leaders are obligated to respect the privacy and security of the
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
|
|
@ -4,15 +4,9 @@ Thank you for your interest in building the coziest web experience for people li
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
You will need [to install nodejs](https://nodejs.org/en/download) and [pnpm](https://pnpm.io/installation) if you do not have them yet in your machine.
|
|
||||||
|
|
||||||
This project is built with the following tech
|
This project is built with the following tech
|
||||||
|
|
||||||
1. TypeScript, HTML, CSS - even just knowing basic JS and CSS will give you familiarity of the code
|
|
||||||
1. [Astro](https://astro.build) - Astro is the chosen framework, please read throught the basics on their docs if you are unfamiliar
|
1. [Astro](https://astro.build) - Astro is the chosen framework, please read throught the basics on their docs if you are unfamiliar
|
||||||
|
1. [Vite](https://vitejs.dev) - Vite is the bundler Astro is using and it allows us to do server-side rendering
|
||||||
- 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
|
||||||
|
@ -23,17 +17,17 @@ This project is built with the following tech
|
||||||
git clone https://github.com/ayoayco/cozy.git
|
git clone https://github.com/ayoayco/cozy.git
|
||||||
```
|
```
|
||||||
|
|
||||||
2. install dependencies using `pnpm`
|
2. install dependencies
|
||||||
|
|
||||||
```
|
```
|
||||||
cd cozy
|
cd cozy
|
||||||
pnpm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
3. run the dev server
|
3. run the dev server
|
||||||
|
|
||||||
```
|
```
|
||||||
pnpm start
|
npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
## Good first issues
|
## Good first issues
|
||||||
|
|
51
Dockerfile
|
@ -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"]
|
|
78
README.md
|
@ -1,30 +1,18 @@
|
||||||
<p align="center">
|
<h1 align="center"><s>Cozy 🧸</s> Uh, we need a new name! See why <a href="https://social.ayco.io/@ayo/110547172702258070">here</a></h1>
|
||||||
<img src="https://github.com/user-attachments/assets/e49b56a7-cc0f-45a3-98e0-8bbcbd02a47c" alt="COZY logo" /><br />
|
|
||||||
Remove distractions. Save for later.<br />
|
|
||||||
Cozy is your modern-day reading companion.
|
|
||||||
</p>
|
|
||||||
<p align="center">
|
<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 />
|
||||||
|
<a href="https://cozy-reader.netlify.app" target="_blank"><img src="https://raw.githubusercontent.com/ayoayco/cozy/main/.github/assets/screenshot.png" /></a><br />
|
||||||
|
<a href="https://cozy-reader.netlify.app" target="_blank">↗ Try the App! ↗</a><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?
|
||||||
|
Well here's what I’m building:
|
||||||
Here's what this project is building:
|
1. An app that just works, no sign ups or setups.
|
||||||
|
|
||||||
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
|
||||||
|
@ -33,65 +21,43 @@ Here's what this project is building:
|
||||||
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 |
|
||||||
| Feature | Status | Version |
|
| --- | --- |
|
||||||
| ---------------------- | -------------- | ------- |
|
| Remove distractions| ✅ DONE |
|
||||||
| Remove distractions | ✅ DONE | v0.0.x |
|
| Save viewed history | ✅ DONE |
|
||||||
| Save viewed history | ✅ DONE | v0.1.x |
|
| Set items as Favorites | 🛠️ In-progress |
|
||||||
| Open links within Cozy | ✅ DONE | v0.2.x |
|
| PWA: full Offline access | |
|
||||||
| Offline access | ✅ DONE | v0.3.x |
|
| AI insights | |
|
||||||
| Set items as Favorites | 🛠️ In-progress | v0.4.x |
|
| Browser Extensions | |
|
||||||
| Smart insights | | v0.5.x |
|
| Native Apps | |
|
||||||
| Browser Extensions | | |
|
|
||||||
| 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](https://cozy-reader.netlify.app/) 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-reader.netlify.app/?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 |
|
||||||
| --------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
| --- | --- |
|
||||||
|  |  |
|
|  |  |
|
||||||
|
|
||||||
**3. One-click Extension then...**
|
**3. One-click Extension then...**
|
||||||
|
|
||||||
It doesn't exist yet... I'll probably get to creating a browser extension at some point. But for now, PRs welcome! :)
|
It doesn't exist yet... I'll probably get to creating a browser extension at some point. But for now, PRs welcome! :)
|
||||||
|
|
||||||
## Credits
|
|
||||||
|
|
||||||
We are thankful for all the building blocks provided by the following projects:
|
|
||||||
|
|
||||||
1. [Astro](https://www.npmjs.com/package/astro) for our server-side rendering framework
|
|
||||||
1. [@ayco/astro-sw](https://ayco.io/n/@ayco/astro-sw) for taking app's service worker, and injecting needed dynamic assets & variables
|
|
||||||
1. [@ayco/astro-resume](https://ayco.io/n/@ayco/astro-resume) for serializing server data to be used in the client
|
|
||||||
1. [@extractus/article-extractor](https://www.npmjs.com/package/@extractus/article-extractor) for the amazing scraping of articles
|
|
||||||
1. [astro-iconify](https://www.npmjs.com/package/astro-iconify) for easily using icon-sets in [iconify](https://icon-sets.iconify.design/)
|
|
||||||
1. [ultrahtml](https://www.npmjs.com/package/ultrahtml) for any cleanup and transformation we do on the received article content
|
|
||||||
1. [fastify](https://fastify.dev/) for our production server and [nginx](https://nginx.org/) as reverse proxy
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
Speaking of PRs being welcome, see our [CONTRIBUTING guide](/CONTRIBUTING.md).
|
||||||
|
|
||||||
If any of the above seems to need improvement for you, we are always eager to hear feedback and welcome all contributions. See our [CONTRIBUTING](/CONTRIBUTING.md) guide for more info.
|
🧸
|
||||||
|
|
||||||
Join our [Discord](https://discord.gg/kkvW7GYNAp) if you need help!
|
|
||||||
|
|
|
@ -1,34 +1,8 @@
|
||||||
// @ts-check
|
import { defineConfig } from "astro/config";
|
||||||
import { defineConfig } from 'astro/config'
|
import netlify from "@astrojs/netlify/functions";
|
||||||
import node from '@astrojs/node'
|
|
||||||
import sitemap from '@astrojs/sitemap'
|
|
||||||
import serviceWorker from '@ayco/astro-sw'
|
|
||||||
|
|
||||||
import { VERSION } from './src/consts'
|
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
output: 'static',
|
output: "server",
|
||||||
site: 'https://cozy.pub/',
|
adapter: netlify()
|
||||||
adapter: node({
|
});
|
||||||
mode: 'middleware',
|
|
||||||
}),
|
|
||||||
vite: {
|
|
||||||
server: {
|
|
||||||
fs: {
|
|
||||||
strict: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
integrations: [
|
|
||||||
sitemap(),
|
|
||||||
serviceWorker({
|
|
||||||
path: './src/sw.mjs',
|
|
||||||
assetCachePrefix: 'cozy-reader',
|
|
||||||
assetCacheVersionID: VERSION,
|
|
||||||
logAssets: true,
|
|
||||||
esbuild: {
|
|
||||||
minify: true,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
})
|
|
Before Width: | Height: | Size: 21 KiB |
|
@ -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 |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 20 KiB |
|
@ -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 |
Before Width: | Height: | Size: 7 KiB |
|
@ -1,7 +0,0 @@
|
||||||
services:
|
|
||||||
cozy:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
ports:
|
|
||||||
- 4321:4321
|
|
|
@ -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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
|
@ -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
|
|
|
@ -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 |
6764
package-lock.json
generated
Normal file
72
package.json
|
@ -1,73 +1,17 @@
|
||||||
{
|
{
|
||||||
"name": "@ayco/cozy",
|
"name": "@ayco/cozy",
|
||||||
"version": "0.3.3",
|
"version": "0.1.27",
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/ayoayco/cozy"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
},
|
|
||||||
"homepage": "https://cozy.pub",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "astro dev",
|
"start": "astro dev",
|
||||||
"dev": "astro dev",
|
"build": "astro build"
|
||||||
"build": "astro telemetry disable && astro build",
|
|
||||||
"build:server": "esbuild server.mjs --bundle --platform=node --packages=external --format=esm --outfile=server-bundle.mjs",
|
|
||||||
"preview": "node ./server.mjs",
|
|
||||||
"build:preview": "npm run build && npm run preview",
|
|
||||||
"publish:patch": "npm version patch && npm publish --access public",
|
|
||||||
"publish:minor": "npm version minor && npm publish --access public",
|
|
||||||
"deploy:client": "npm run build && scp -r dist/client/ ayo@ayco.io:~/cozy/dist/",
|
|
||||||
"test": "vitest",
|
|
||||||
"prepare": "husky && husky install",
|
|
||||||
"lint": "eslint . --config eslint.config.mjs --cache",
|
|
||||||
"format": "prettier . --write"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@astrojs/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",
|
"astro": "^2.5.7"
|
||||||
"@eslint/compat": "^1.3.1",
|
|
||||||
"@eslint/js": "^9.32.0",
|
|
||||||
"@extractus/article-extractor": "^8.0.19",
|
|
||||||
"astro-eslint-parser": "^1.2.2",
|
|
||||||
"esbuild": "^0.25.8",
|
|
||||||
"eslint": "^9.32.0",
|
|
||||||
"eslint-config-prettier": "^10.1.8",
|
|
||||||
"eslint-plugin-astro": "^1.3.1",
|
|
||||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
||||||
"eslint-plugin-prettier": "^5.5.4",
|
|
||||||
"globals": "^16.3.0",
|
|
||||||
"husky": "^9.1.7",
|
|
||||||
"lint-staged": "^16.1.4",
|
|
||||||
"prettier": "^3.6.2",
|
|
||||||
"prettier-plugin-astro": "^0.14.1",
|
|
||||||
"sass": "^1.90.0",
|
|
||||||
"typescript": "^5.9.2",
|
|
||||||
"typescript-eslint": "^8.39.0",
|
|
||||||
"vitest": "^3.2.4"
|
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"dependencies": {
|
||||||
"*.{js,mjs,astro,ts}": [
|
"@astrojs/netlify": "^2.2.2",
|
||||||
"prettier --write",
|
"@extractus/article-extractor": "^7.2.15",
|
||||||
"eslint --fix"
|
"astro-iconify": "^1.2.0",
|
||||||
],
|
"sass": "^1.62.1"
|
||||||
"*.json": [
|
|
||||||
"prettier --write"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
8424
pnpm-lock.yaml
|
@ -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
|
|
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 84 KiB |
BIN
public/cozy.jpg
Before Width: | Height: | Size: 180 KiB |
|
@ -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 |
|
@ -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 |
|
@ -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"
|
|
||||||
}
|
|
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 21 KiB |
107
purge-job.cron
|
@ -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
|
|
41
server.mjs
|
@ -1,41 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
import Fastify from 'fastify'
|
|
||||||
import fastifyMiddie from '@fastify/middie'
|
|
||||||
import fastifyStatic from '@fastify/static'
|
|
||||||
import { fileURLToPath } from 'node:url'
|
|
||||||
import { handler as ssrHandler } from './dist/server/entry.mjs'
|
|
||||||
|
|
||||||
const app = Fastify({ logger: true })
|
|
||||||
|
|
||||||
await app
|
|
||||||
.register(import('@fastify/rate-limit'), {
|
|
||||||
global: true,
|
|
||||||
max: 25,
|
|
||||||
timeWindow: 1000 * 60 * 5,
|
|
||||||
})
|
|
||||||
.register(fastifyStatic, {
|
|
||||||
root: fileURLToPath(new URL('./dist/client', import.meta.url)),
|
|
||||||
})
|
|
||||||
.register(fastifyMiddie)
|
|
||||||
|
|
||||||
app.use(ssrHandler)
|
|
||||||
|
|
||||||
await app.setNotFoundHandler(
|
|
||||||
{
|
|
||||||
preHandler: app.rateLimit(),
|
|
||||||
},
|
|
||||||
function (request, reply) {
|
|
||||||
reply.code(404).send({ nothing: 'to see here' })
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
await app.setErrorHandler(function (error, request, reply) {
|
|
||||||
if (error.statusCode === 429) {
|
|
||||||
reply.code(429)
|
|
||||||
error.message = 'You hit the rate limit! Slow down please!'
|
|
||||||
}
|
|
||||||
reply.send(error)
|
|
||||||
})
|
|
||||||
|
|
||||||
app.listen({ port: 4321 })
|
|
|
@ -1,135 +1,118 @@
|
||||||
---
|
---
|
||||||
import Icon from 'astro-iconify'
|
import Icon from 'astro-iconify';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
url: string | null
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const placeholder = 'Type the article URL here'
|
const { url } = Astro.props;
|
||||||
const { url } = Astro.props
|
const placeholder = 'Type the article URL here';
|
||||||
---
|
---
|
||||||
|
|
||||||
<div id="address-bar">
|
<div class="address-bar">
|
||||||
<form>
|
<form>
|
||||||
<label for="app-url">
|
<button aria-label="Back" class="left-buttons" type="button" id="app-back" name="app-back" onclick="history.go(-1); return false;" hidden>
|
||||||
<Icon name="ic:round-arrow-forward-ios" />
|
<Icon name="ic:round-arrow-back-ios" />
|
||||||
</label>
|
</button>
|
||||||
<input
|
<input type="url" id="app-url" name="url" value={url} placeholder={placeholder} required />
|
||||||
type="url"
|
<button aria-label="Submit" class="right-buttons" type="submit" id="submit">
|
||||||
id="app-url"
|
|
||||||
name="url"
|
|
||||||
value={url ?? ''}
|
|
||||||
placeholder={placeholder}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
aria-label="Submit"
|
|
||||||
class="btn right-buttons primary"
|
|
||||||
type="submit"
|
|
||||||
id="submit"
|
|
||||||
>
|
|
||||||
<Icon name="ri:ai-generate" />
|
<Icon name="ri:ai-generate" />
|
||||||
</button>
|
</button>
|
||||||
<a
|
<button aria-label="Home" class="right-buttons" type="button" id="app-home" name="app-home" onclick="window.location.href='/'; return false;" hidden>
|
||||||
aria-label="Home"
|
<Icon name="ic:round-library-books" />
|
||||||
class="btn right-buttons"
|
</button>
|
||||||
type="button"
|
<button aria-label="GitHub" class="right-buttons" type="button" id="gh-link" onclick="window.open('https://github.com/ayoayco/cozy', '_blank');" hidden>
|
||||||
id="app-home"
|
<Icon name="mdi:github" />
|
||||||
href="/"
|
</button>
|
||||||
>
|
|
||||||
<Icon name="mdi:home" />
|
|
||||||
</a>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<script>
|
||||||
#address-bar {
|
const ghLink = document.getElementById('gh-link') as HTMLButtonElement;
|
||||||
width: 100%;
|
const backLink = document.getElementById('app-back') as HTMLButtonElement;
|
||||||
position: relative;
|
const homeLink = document.getElementById('app-home') as HTMLButtonElement;
|
||||||
|
const urlInput = document.getElementById('app-url') as HTMLInputElement;
|
||||||
|
const submitBtn = document.getElementById('submit') as HTMLButtonElement;
|
||||||
|
|
||||||
|
// if js is enabled, show the back button and github link
|
||||||
|
ghLink.removeAttribute('hidden');
|
||||||
|
backLink.removeAttribute('hidden');
|
||||||
|
homeLink.removeAttribute('hidden');
|
||||||
|
|
||||||
|
if (urlInput.value === '') {
|
||||||
|
backLink.setAttribute('disabled', 'true');
|
||||||
|
submitBtn.setAttribute('disabled', 'true');
|
||||||
|
homeLink.setAttribute('disabled', 'true');
|
||||||
}
|
}
|
||||||
|
|
||||||
form:has(input[type='url']:focus) {
|
urlInput.addEventListener('input', (e) => {
|
||||||
border-color: var(--accent);
|
const target = e.target as HTMLInputElement;
|
||||||
box-shadow: 0 1px 10px 0px var(--accent);
|
if (target.value === '') {
|
||||||
|
submitBtn.setAttribute('disabled', 'true');
|
||||||
|
} else {
|
||||||
|
submitBtn.removeAttribute('disabled');
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
form {
|
<style lang="scss">
|
||||||
width: 100%;
|
.address-bar {
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-radius: 30px;
|
width: 100%;
|
||||||
border: 2px solid rgb(var(--gray));
|
}
|
||||||
background-color: white;
|
|
||||||
box-shadow: 0 1px 3px 1px rgb(var(--gray-light));
|
:global(form) {
|
||||||
|
width: 100%;
|
||||||
|
border: 0px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
box-shadow: 0 1px 3px 1px #eee;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
input[type='url']:focus {
|
:global(input[type="url"]) {
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type='url'] {
|
|
||||||
flex: 3;
|
flex: 3;
|
||||||
border: 0px;
|
background-color: white;
|
||||||
border-radius: 30px;
|
border-radius: 5px;
|
||||||
font-size: normal;
|
border: 1px solid #eee;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
color: rgb(var(--black));
|
color: #555;
|
||||||
caret-color: var(--accent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
:global(button#app-home),
|
||||||
display: block;
|
:global(button#app-back),
|
||||||
border: 0px;
|
:global(button#submit),
|
||||||
height: 100%;
|
:global(button#gh-link) {
|
||||||
vertical-align: middle;
|
|
||||||
background-color: transparent;
|
|
||||||
padding: 0.5rem 0;
|
|
||||||
color: rgb(var(--gray));
|
|
||||||
|
|
||||||
svg {
|
|
||||||
border: 0px;
|
border: 0px;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
width: 24px;
|
padding: 0px;
|
||||||
height: 24px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
color: black;
|
||||||
}
|
|
||||||
|
|
||||||
.btn.primary {
|
:global(svg) {
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
color: rgb(var(--gray));
|
|
||||||
display: block;
|
|
||||||
border: 0px;
|
|
||||||
height: 100%;
|
|
||||||
vertical-align: middle;
|
|
||||||
background-color: transparent;
|
|
||||||
padding: 0.5rem 0;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
border: 0px;
|
border: 0px;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
width: 24px;
|
width: 1.5rem;
|
||||||
height: 24px;
|
height: 1.5rem;
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-buttons {
|
:global(.left-buttons) {
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
.right-buttons {
|
:global(.right-buttons) {
|
||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn svg:hover {
|
:global(button:hover),
|
||||||
|
:global(button.primary-button) {
|
||||||
color: blue !important;
|
color: blue !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn[disabled='true'] svg {
|
:global(button[disabled="true"]) {
|
||||||
color: rgb(var(--gray-light)) !important;
|
color: #ccc !important;
|
||||||
cursor: default !important;
|
cursor: default !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,44 +1,34 @@
|
||||||
---
|
---
|
||||||
import Icon from 'astro-iconify'
|
import Icon from 'astro-iconify'
|
||||||
import { VERSION } from '../consts'
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<footer>
|
<footer class="footer">
|
||||||
<section>Remove distractions. Save for later.</section>
|
|
||||||
|
|
||||||
<section class="attribution">
|
<p>Remove distractions. Save for later.</p>
|
||||||
<a href="/blog/01-building-a-cozy-web/">Hand-crafted</a> with <Icon
|
<p>No sign-ups. No setups. It just works.</p>
|
||||||
name="line-md:heart"
|
<p>
|
||||||
/> by <a href="https://ayo.ayco.io">Ayo Ayco</a>
|
<a href="https://github.com/ayoayco/cozy">Star on GitHub to support!</a>
|
||||||
<br />
|
</p>
|
||||||
<a href="/blog">Blog</a> •
|
|
||||||
<a href="https://ayco.io/sh/cozy">SourceHut</a> •
|
<p class="footer__attribution">
|
||||||
<a href="https://social.ayco.io/@ayo">Mastodon</a>
|
<a href="http://ayos.blog/building-a-cozy-web/">Hand-crafted</a> with <Icon name="line-md:heart" /> by <a href="https://ayo.ayco.io">Ayo Ayco</a
|
||||||
<br />
|
>
|
||||||
<span>{VERSION}</span>
|
</p>
|
||||||
</section>
|
|
||||||
|
<p class="footer__disclaimer">All content rights and credits are reserved to their respective owners.</p>
|
||||||
|
|
||||||
<section class="disclaimer">All rights reserved to content owners.</section>
|
|
||||||
</footer>
|
</footer>
|
||||||
|
<style lang="scss">
|
||||||
<style>
|
.footer {
|
||||||
footer {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-width: 350px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: rgb(var(--gray));
|
color: #333;
|
||||||
display: flex;
|
|
||||||
font-size: small;
|
font-size: small;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
|
||||||
& 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,20 +37,13 @@ import { VERSION } from '../consts'
|
||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
& section {
|
&__disclaimer, &__attribution {
|
||||||
flex: 1;
|
padding: 1rem 0 0;
|
||||||
padding: 1rem 1rem 0;
|
|
||||||
|
|
||||||
&.attribution {
|
|
||||||
flex: 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
&__title {
|
||||||
footer {
|
font-weight: bold;
|
||||||
flex-direction: column;
|
font-size: 1.2rem;
|
||||||
max-width: 350px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -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>
|
|
|
@ -1,230 +1,174 @@
|
||||||
---
|
---
|
||||||
import Serialize from '@ayco/astro-resume'
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
|
postDivSelector: string,
|
||||||
skipSave?: boolean
|
skipSave?: boolean
|
||||||
}
|
}
|
||||||
---
|
|
||||||
|
|
||||||
|
const {postDivSelector, skipSave = false} = Astro.props;
|
||||||
|
---
|
||||||
<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} />
|
<input value={postDivSelector} name="postDivSelector" id="postDivSelector" hidden />
|
||||||
|
<input type="checkbox" id="skipSave" name="skipSave" checked={skipSave} hidden />
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getPostCard, renderPost } from '../utils/library'
|
import { getPostCard, renderPost } from '../utils/library'
|
||||||
import { cozify } from '../utils/sanitizer'
|
const cache = await caches.open('cozy-reader');
|
||||||
import { deserialize } from '@ayco/astro-resume'
|
const url = new URL(window.location.href);
|
||||||
const cache = await caches.open('cozy-reader')
|
const response = await cache.match(url)
|
||||||
const baseUrl = window.location.origin + '/a'
|
const postDivSelector = document.getElementById('postDivSelector') as HTMLInputElement;
|
||||||
let url = new URL(window.location.href)
|
const skipSave = document.getElementById('skipSave') as HTMLInputElement;
|
||||||
// only cached unencoded url param
|
|
||||||
const urlParam = url.searchParams.get('url')
|
if (!response) {
|
||||||
if (urlParam) {
|
if (!skipSave?.checked) {
|
||||||
url = new URL(`${url.origin}/?url=${urlParam}`)
|
await cache.add(url);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { skipSave } = deserialize('preferences') ?? ''
|
const cachedRequests = (await cache.keys())
|
||||||
|
.filter(request => {
|
||||||
|
const urlObj = new URL(request.url);
|
||||||
|
return urlObj.search !== '' && urlObj.searchParams.get('url') !== '';
|
||||||
|
});
|
||||||
|
|
||||||
const routerOutlet = 'router-outlet'
|
if(cachedRequests?.length) {
|
||||||
const includesAppURL = urlParam?.includes(baseUrl) ?? false
|
const list = document.querySelector('#post-list');
|
||||||
|
const heading = document.querySelector('#library span#heading') as HTMLHeadingElement;
|
||||||
|
heading.innerHTML = 'History';
|
||||||
|
|
||||||
try {
|
cachedRequests
|
||||||
if (
|
.reverse()
|
||||||
url.href.slice(0, url.href.length - 1) !== baseUrl &&
|
.forEach(async (request) => {
|
||||||
!skipSave &&
|
const {url} = request;
|
||||||
!includesAppURL
|
const link = document.createElement('a');
|
||||||
) {
|
|
||||||
console.info('adding one to cache', {
|
|
||||||
context: 'cozy-reader',
|
|
||||||
data: url,
|
|
||||||
})
|
|
||||||
await cache.add(url)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('ERR', { context: 'cozy-reader', data: error })
|
|
||||||
}
|
|
||||||
|
|
||||||
const cachedRequests = (await cache.keys()).filter((request) => {
|
let responseText;
|
||||||
const urlObj = new URL(request.url)
|
|
||||||
const urlParam = urlObj.searchParams.get('url')
|
|
||||||
|
|
||||||
return (
|
|
||||||
urlObj.search !== '' &&
|
|
||||||
!urlParam?.startsWith(baseUrl) &&
|
|
||||||
urlParam !== '' &&
|
|
||||||
urlParam !== 'null'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (cachedRequests?.length && routerOutlet !== null) {
|
|
||||||
const list = document.querySelector('#post-list')
|
|
||||||
const heading = document.querySelector(
|
|
||||||
'#library span#heading'
|
|
||||||
) as HTMLHeadingElement
|
|
||||||
heading.innerHTML = 'History'
|
|
||||||
|
|
||||||
cachedRequests.reverse().forEach(async (request) => {
|
|
||||||
const { url } = request
|
|
||||||
const link = document.createElement('a')
|
|
||||||
|
|
||||||
let responseText
|
|
||||||
const fullResponse = await cache.match(url)
|
const fullResponse = await cache.match(url)
|
||||||
if (
|
fullResponse?.text().then(data => {
|
||||||
!fullResponse &&
|
responseText = data;
|
||||||
url.slice(0, url.length - 1) !== baseUrl &&
|
const html = document.createElement('html');
|
||||||
!skipSave &&
|
html.innerHTML = responseText;
|
||||||
!includesAppURL
|
const title = html.querySelector('meta[property="cozy:title"]')?.getAttribute('content');
|
||||||
) {
|
|
||||||
console.info('updating cached', { context: 'cozy-reader', data: url })
|
|
||||||
await cache.add(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
fullResponse?.text().then(async (data) => {
|
|
||||||
responseText = data
|
|
||||||
const cleanedResponse = await cozify(responseText, baseUrl)
|
|
||||||
const html = document.createElement('html')
|
|
||||||
html.innerHTML = cleanedResponse
|
|
||||||
const title = html
|
|
||||||
.querySelector('meta[property="cozy:title"]')
|
|
||||||
?.getAttribute('content')
|
|
||||||
if (title === 'Something is not right') {
|
if (title === 'Something is not right') {
|
||||||
cache.delete(url)
|
cache.delete(url);
|
||||||
return // temporary fix for deleting cached errors
|
return; // temporary fix for deleting cached errors
|
||||||
}
|
}
|
||||||
const postCard = getPostCard(html)
|
const postCard = getPostCard(html);
|
||||||
link.innerHTML = postCard
|
link.innerHTML = postCard;
|
||||||
|
|
||||||
link.href = url
|
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', {
|
renderPost(responseText, url, postDivSelector?.value)
|
||||||
context: 'cozy-reader',
|
|
||||||
data: url,
|
|
||||||
})
|
|
||||||
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;
|
||||||
|
console.log('>>> ishome', isHome);
|
||||||
} 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(data => {
|
||||||
const responseText = data
|
const responseText = data;
|
||||||
const cleanedResponse = await cozify(responseText, baseUrl)
|
renderPost(responseText, url, postDivSelector?.value, true);
|
||||||
console.info('using cached response', {
|
|
||||||
context: 'cozy-reader',
|
|
||||||
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 {
|
:global(span#heading) {
|
||||||
color: #555;
|
color: #555;
|
||||||
font-size: small;
|
font-size: small;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#post-list {
|
#post-list {
|
||||||
list-style: none;
|
|
||||||
padding-left: 0;
|
|
||||||
display: grid;
|
|
||||||
gap: 1em;
|
|
||||||
|
|
||||||
li {
|
:global(li) {
|
||||||
a {
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.post-card__content {
|
:global(.post-card__content) {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-height: calc(70px + 0.5rem);
|
min-height: calc(70px + 0.5rem);
|
||||||
}
|
}
|
||||||
|
:global(.post-card__title, .post-card__description) {
|
||||||
.post-card__title,
|
|
||||||
.post-card__description {
|
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
:global(.post-card__meta, .post-card__description){
|
||||||
.post-card__meta,
|
|
||||||
.post-card__description {
|
|
||||||
font-size: smaller;
|
font-size: smaller;
|
||||||
color: #555;
|
color: #555;
|
||||||
}
|
}
|
||||||
|
:global(.post-card__meta) {
|
||||||
.post-card__meta {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-top: 0;
|
|
||||||
|
|
||||||
* {
|
* {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-card__source {
|
:global(.post-card__source) {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-card__published {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,27 +1,17 @@
|
||||||
---
|
---
|
||||||
import { ArticleData } from '@extractus/article-extractor'
|
import { ArticleData } from "@extractus/article-extractor";
|
||||||
import { cozify } from '../utils/sanitizer'
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
article: ArticleData | null
|
article: ArticleData;
|
||||||
}
|
}
|
||||||
|
|
||||||
const error: ArticleData = {
|
const { article } = Astro.props;
|
||||||
title: 'Something is not right',
|
|
||||||
content: '<p>The article extractor did not get any information.</p>',
|
|
||||||
}
|
|
||||||
let { article } = Astro.props
|
|
||||||
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)
|
|
||||||
---
|
---
|
||||||
|
<div id="post">
|
||||||
<!--
|
{
|
||||||
Changing anything inside the #post div can cause a difference in the cached version of the post in users' devices. For this reason, we should avoid changing the HTML and instead do it with CSS when possible.
|
article && article.url !== '/' &&
|
||||||
-->{
|
<>
|
||||||
article && article.url !== '/' && (
|
|
||||||
<article id="post">
|
|
||||||
{article.source && <span class="source">{article.source}</span>}
|
{article.source && <span class="source">{article.source}</span>}
|
||||||
{article.title && <h1 class="title">{article.title}</h1>}
|
{article.title && <h1 class="title">{article.title}</h1>}
|
||||||
{(article.author || datePublished) && (
|
{(article.author || datePublished) && (
|
||||||
|
@ -30,52 +20,40 @@ const cleanContent = await cozify(article.content ?? '', Astro.url.origin)
|
||||||
{datePublished && <li>️{datePublished}</li>}
|
{datePublished && <li>️{datePublished}</li>}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
<content set:html={cleanContent} />
|
<content set:html={article.content} />
|
||||||
</article>
|
</>
|
||||||
)
|
}
|
||||||
}
|
</div>
|
||||||
|
|
||||||
|
<style is:global lang="scss">
|
||||||
|
|
||||||
|
@counter-style publish-icons {
|
||||||
|
system: cyclic;
|
||||||
|
symbols: "️✍️" "🗓️";
|
||||||
|
suffix: " ";
|
||||||
|
}
|
||||||
|
|
||||||
<style>
|
|
||||||
#post {
|
|
||||||
h1.title {
|
h1.title {
|
||||||
font-size: xx-large;
|
font-size: xx-large;
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h1,
|
span.source {
|
||||||
h2,
|
font-weight: bolder;
|
||||||
h3,
|
|
||||||
h4,
|
|
||||||
h5,
|
|
||||||
h6 {
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.source,
|
|
||||||
.publish-info {
|
|
||||||
font-size: smaller;
|
|
||||||
color: #555;
|
color: #555;
|
||||||
}
|
}
|
||||||
|
|
||||||
.source {
|
ul.publish-info {
|
||||||
font-weight: bold;
|
margin: 0.3em -0.7em 1em;
|
||||||
}
|
list-style: publish-icons;
|
||||||
|
|
||||||
.publish-info {
|
|
||||||
padding-left: 0;
|
|
||||||
margin: 0;
|
|
||||||
list-style: none;
|
|
||||||
|
|
||||||
li {
|
li {
|
||||||
margin: 0;
|
color: #555;
|
||||||
|
font-size: small;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
content {
|
content {
|
||||||
p,
|
p, table, ul, img {
|
||||||
table,
|
|
||||||
ul,
|
|
||||||
img {
|
|
||||||
margin: 1em 0 !important;
|
margin: 1em 0 !important;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
@ -83,8 +61,7 @@ const cleanContent = await cozify(article.content ?? '', Astro.url.origin)
|
||||||
table {
|
table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
|
||||||
td,
|
td, th {
|
||||||
th {
|
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
}
|
}
|
||||||
|
@ -92,21 +69,11 @@ const cleanContent = await cozify(article.content ?? '', Astro.url.origin)
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
&:has(code) {
|
}
|
||||||
|
pre:has(code) {
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
background: #f6f8fa;
|
background: #f6f8fa;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
p,
|
|
||||||
table,
|
|
||||||
ul,
|
|
||||||
img {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
|
@ -1,67 +0,0 @@
|
||||||
---
|
|
||||||
import '../../styles/reset.css'
|
|
||||||
import '../../styles/variables.css'
|
|
||||||
import '../../styles/blog.css'
|
|
||||||
import { SITE_TITLE, SITE_AUTHOR, SITE_DESCRIPTION } from '../../consts'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
isArticle?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
let {
|
|
||||||
isArticle = false,
|
|
||||||
title,
|
|
||||||
description = 'default description',
|
|
||||||
} = Astro.props
|
|
||||||
|
|
||||||
description =
|
|
||||||
title === SITE_TITLE ? SITE_DESCRIPTION : `${description} • ${SITE_TITLE}`
|
|
||||||
---
|
|
||||||
|
|
||||||
<!-- Global Metadata -->
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
||||||
|
|
||||||
<!-- Icons -->
|
|
||||||
<link rel="icon" href="favicon.svg" />
|
|
||||||
<link rel="mask-icon" href="mask-icon.svg" color="#000000" />
|
|
||||||
<link rel="apple-touch-icon" href="apple-touch-icon.png" />
|
|
||||||
<meta name="generator" content={Astro.generator} />
|
|
||||||
|
|
||||||
<!-- Font preloads -->
|
|
||||||
<link
|
|
||||||
rel="preload"
|
|
||||||
href="/fonts/atkinson-regular.woff"
|
|
||||||
as="font"
|
|
||||||
type="font/woff"
|
|
||||||
crossorigin
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="preload"
|
|
||||||
href="/fonts/atkinson-bold.woff"
|
|
||||||
as="font"
|
|
||||||
type="font/woff"
|
|
||||||
crossorigin
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Primary Meta Tags -->
|
|
||||||
<title>{title} • {description}</title>
|
|
||||||
<meta name="title" content={title} />
|
|
||||||
<meta name="description" content={description} />
|
|
||||||
|
|
||||||
<!-- Open Graph / Facebook -->
|
|
||||||
{
|
|
||||||
isArticle ? (
|
|
||||||
<meta property="og:type" content="article" />
|
|
||||||
) : (
|
|
||||||
<meta property="og:type" content="website" />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<meta property="og:url" content={Astro.url} />
|
|
||||||
<meta property="og:title" content={title} />
|
|
||||||
<meta property="og:description" content={description} />
|
|
||||||
<meta property="og:image" content="/touch-icon-large.png" />
|
|
||||||
<meta property="og:site_name" content={SITE_TITLE} />
|
|
||||||
<meta property="article:author" content={SITE_AUTHOR} />
|
|
|
@ -1,81 +0,0 @@
|
||||||
---
|
|
||||||
import {
|
|
||||||
SITE_AUTHOR,
|
|
||||||
SITE_AUTHOR_EMAIL,
|
|
||||||
SITE_AUTHOR_MASTODON,
|
|
||||||
SITE_PROJECT_REPO,
|
|
||||||
} from '../../consts'
|
|
||||||
const today = new Date()
|
|
||||||
---
|
|
||||||
|
|
||||||
<footer>
|
|
||||||
<p>© {today.getFullYear()} {SITE_AUTHOR}. All rights reserved.</p>
|
|
||||||
<p>
|
|
||||||
Want to get in touch? Send a mail to <a href={`mailto:${SITE_AUTHOR_EMAIL}`}
|
|
||||||
>Cozy at ayco.io</a
|
|
||||||
>.
|
|
||||||
</p>
|
|
||||||
<div class="social-links">
|
|
||||||
<a href={SITE_AUTHOR_MASTODON} target="_blank">
|
|
||||||
<span class="sr-only">Follow Ayo on Mastodon</span>
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
aria-hidden="true"
|
|
||||||
width="32"
|
|
||||||
height="32"
|
|
||||||
astro-icon="social/mastodon"
|
|
||||||
><path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a3.614 3.614 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522c0-.859.22-1.541.66-2.046.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764.442.505.661 1.187.661 2.046v4.203z"
|
|
||||||
></path></svg
|
|
||||||
>
|
|
||||||
</a>
|
|
||||||
<a href={SITE_PROJECT_REPO} target="_blank">
|
|
||||||
<span class="sr-only">Go to Cozy's GitHub repo</span>
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
aria-hidden="true"
|
|
||||||
width="32"
|
|
||||||
height="32"
|
|
||||||
astro-icon="social/github"
|
|
||||||
><path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"
|
|
||||||
></path></svg
|
|
||||||
>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
<style>
|
|
||||||
footer {
|
|
||||||
padding: 2em 1em 6em 1em;
|
|
||||||
background: linear-gradient(var(--gray-gradient)) no-repeat;
|
|
||||||
color: rgb(var(--gray));
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
& p {
|
|
||||||
margin-bottom: 0;
|
|
||||||
|
|
||||||
& a {
|
|
||||||
color: rgb(var(--gray));
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: rgb(var(--gray-dark));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.social-links {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 1em;
|
|
||||||
margin-top: 1em;
|
|
||||||
}
|
|
||||||
.social-links a {
|
|
||||||
text-decoration: none;
|
|
||||||
color: rgb(var(--gray));
|
|
||||||
}
|
|
||||||
.social-links a:hover {
|
|
||||||
color: rgb(var(--gray-dark));
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,17 +0,0 @@
|
||||||
---
|
|
||||||
interface Props {
|
|
||||||
date: Date
|
|
||||||
}
|
|
||||||
|
|
||||||
const { date } = Astro.props
|
|
||||||
---
|
|
||||||
|
|
||||||
<time datetime={date.toISOString()}>
|
|
||||||
{
|
|
||||||
date.toLocaleDateString('en-us', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</time>
|
|
|
@ -1,140 +0,0 @@
|
||||||
---
|
|
||||||
import {
|
|
||||||
SITE_AUTHOR_MASTODON,
|
|
||||||
SITE_DESCRIPTION,
|
|
||||||
SITE_PROJECT_REPO,
|
|
||||||
SITE_TITLE,
|
|
||||||
} from '../../consts'
|
|
||||||
---
|
|
||||||
|
|
||||||
<header>
|
|
||||||
<nav>
|
|
||||||
<div class="site-title">
|
|
||||||
<h2><a href="/blog">{SITE_TITLE}</a></h2>
|
|
||||||
<small class="site-description">{SITE_DESCRIPTION}</small>
|
|
||||||
</div>
|
|
||||||
<div class="social-links">
|
|
||||||
<a href="/">
|
|
||||||
<span class="primary-btn">Get Cozy!</span>
|
|
||||||
</a>
|
|
||||||
<a href={SITE_AUTHOR_MASTODON} target="_blank">
|
|
||||||
<span class="sr-only">Follow Ayo on Mastodon</span>
|
|
||||||
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32"
|
|
||||||
><path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a3.614 3.614 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522c0-.859.22-1.541.66-2.046.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764.442.505.661 1.187.661 2.046v4.203z"
|
|
||||||
></path></svg
|
|
||||||
>
|
|
||||||
</a>
|
|
||||||
<a href={SITE_PROJECT_REPO} target="_blank">
|
|
||||||
<span class="sr-only">Go to Cozy's GitHub repo</span>
|
|
||||||
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32"
|
|
||||||
><path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"
|
|
||||||
></path></svg
|
|
||||||
>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
<style>
|
|
||||||
header {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0 1em;
|
|
||||||
background: white;
|
|
||||||
box-shadow: 0 2px 8px rgba(var(--black), 5%);
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: x-large;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 a,
|
|
||||||
h2 a.active {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
nav {
|
|
||||||
width: 900px;
|
|
||||||
max-width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 1em 0.5em;
|
|
||||||
margin: 0 auto;
|
|
||||||
|
|
||||||
& span.primary-btn {
|
|
||||||
background-color: rgba(var(--black), 95%);
|
|
||||||
box-shadow: 0 2px 8px rgba(var(--black), 5%);
|
|
||||||
color: white;
|
|
||||||
border-radius: 5px;
|
|
||||||
display: inline-block;
|
|
||||||
text-align: center;
|
|
||||||
padding: 1px 0.5em 0;
|
|
||||||
transition: 0.2s ease;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nav .social-links a:hover {
|
|
||||||
& span {
|
|
||||||
background-color: var(--accent);
|
|
||||||
box-shadow: 0 2px 8px var(--accent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nav .site-title {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav a,
|
|
||||||
nav .site-description {
|
|
||||||
color: var(--black);
|
|
||||||
border-bottom: 4px solid transparent;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav a {
|
|
||||||
padding: 0 0.5em;
|
|
||||||
}
|
|
||||||
nav a:hover {
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
nav a.active {
|
|
||||||
text-decoration: none;
|
|
||||||
border-bottom-color: var(--accent);
|
|
||||||
}
|
|
||||||
.social-links,
|
|
||||||
.social-links a {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 700px) {
|
|
||||||
nav {
|
|
||||||
display: block;
|
|
||||||
padding: 1em 0;
|
|
||||||
|
|
||||||
& .site-description {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
& span.primary-btn {
|
|
||||||
line-height: 1.5rem;
|
|
||||||
height: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .social-links a {
|
|
||||||
font-size: small;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
height: 1.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
nav a {
|
|
||||||
padding: 0.5em;
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,26 +0,0 @@
|
||||||
---
|
|
||||||
import type { HTMLAttributes } from 'astro/types'
|
|
||||||
|
|
||||||
type Props = HTMLAttributes<'a'>
|
|
||||||
|
|
||||||
const { href, class: className, ...props } = Astro.props
|
|
||||||
|
|
||||||
const { pathname } = Astro.url
|
|
||||||
// eslint-disable-next-line no-useless-escape
|
|
||||||
const subpath = pathname.match(/[^\/]+/g)
|
|
||||||
const isActive = href === pathname || href === '/' + subpath?.[0]
|
|
||||||
---
|
|
||||||
|
|
||||||
<a href={href} class:list={[className, { active: isActive }]} {...props}>
|
|
||||||
<slot />
|
|
||||||
</a>
|
|
||||||
<style>
|
|
||||||
a {
|
|
||||||
display: inline-block;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
a.active {
|
|
||||||
font-weight: bolder;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,9 +0,0 @@
|
||||||
export const SITE_TITLE = 'Cozy Blog'
|
|
||||||
export const SITE_AUTHOR = 'Ayo Ayco'
|
|
||||||
export const SITE_AUTHOR_URL = 'https://ayo.ayco.io'
|
|
||||||
export const SITE_AUTHOR_EMAIL = 'cozy@ayco.io'
|
|
||||||
export const SITE_AUTHOR_MASTODON = 'https://social.ayco.io/@ayo'
|
|
||||||
export const SITE_PROJECT_REPO = 'https://github.com/ayoayco/Cozy'
|
|
||||||
export const SITE_DESCRIPTION = 'The Web is Yours.'
|
|
||||||
|
|
||||||
export const VERSION = 'Drooling-Dogs'
|
|
|
@ -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 }
|
|
|
@ -1,67 +0,0 @@
|
||||||
---
|
|
||||||
title: Building a Cozy Web
|
|
||||||
description: Let us build the web we want!
|
|
||||||
pubDate: 'Aug 14 2024'
|
|
||||||
heroImage: '/cozy.jpg'
|
|
||||||
---
|
|
||||||
|
|
||||||
> This was originally posted on [Ayo's Blog](https://ayos.blog/building-a-cozy-web) last Jun 1, 2023.
|
|
||||||
|
|
||||||
Have you ever clicked a link to an article, all hyped up to read the content, only to be slapped in the face with popups over popups of requests to subscribe and asking consent to track you with cookies?
|
|
||||||
|
|
||||||
Do you sometimes wish you can have a consistent experience when opening articles... a place to save all your favorites, and possibly get helpful insights?
|
|
||||||
|
|
||||||
Ah, well you're not alone. 🤣
|
|
||||||
|
|
||||||
This is exactly why I started [**Cozy** 🧸](https://cozy.pub/).
|
|
||||||
|
|
||||||
It's a simple web page that can make any web page content-focused! 🎉
|
|
||||||
|
|
||||||
It uses a library called [@extractus/article-extractor](https://www.npmjs.com/package/@extractus/article-extractor) to fetch and extract just the content.
|
|
||||||
|
|
||||||
Then with [Astro](https://astro.build), we can server-side render the page so your browser only gets clean HTML!
|
|
||||||
|
|
||||||
No nonsense. No headaches.
|
|
||||||
|
|
||||||
The project and the road map for features are all public on my [GitHub](https://github.com/ayoayco/cozy-reader)
|
|
||||||
|
|
||||||
## Cozy Features
|
|
||||||
|
|
||||||
Right now, it successfully extracts the content and delivers a clean page to your browser.
|
|
||||||
|
|
||||||
I'm working toward bringing the following in the coming weeks:
|
|
||||||
|
|
||||||
1. Save favorites to a library
|
|
||||||
2. Offline access
|
|
||||||
3. Smart Insights about the article
|
|
||||||
4. Easier usage (browser extensions or apps?)
|
|
||||||
|
|
||||||
## Coziest Usage
|
|
||||||
|
|
||||||
The most convenient way to use it right now is through what we call a browser bookmarklet.
|
|
||||||
|
|
||||||
Basically you can have a button there beside your other bookmarks that will open the current page in Cozy.
|
|
||||||
|
|
||||||
You can create this new bookmark titled 'Get cozy!' and put the following as value for the URL:
|
|
||||||
|
|
||||||
```js
|
|
||||||
javascript:(function(){ window.open('https://cozy.pub/?url=%27 + window.location.href, %27_self%27); })();
|
|
||||||
```
|
|
||||||
|
|
||||||
This is possible on all major browsers, including Safari on iOS (where I personally use this often). Some screenshots:
|
|
||||||
|
|
||||||
| Firefox | Chrome |
|
|
||||||
| ---------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
|  |  |
|
|
||||||
|
|
||||||
## Join the Project!
|
|
||||||
|
|
||||||
I'm sure this looks very simple, but I think this is the most exciting hobby project I've started yet.
|
|
||||||
|
|
||||||
There's a lot that happened and a lot of problems could have been avoided if people were equipped to assess the content they find online.
|
|
||||||
|
|
||||||
I think there's lots of good a simple tool could bring if it allows users to cut-through all the distractions and are presented with unbiased and accurate information.
|
|
||||||
|
|
||||||
This project is a groundwork for this experience.
|
|
||||||
|
|
||||||
Let's build the web we want! 🧸
|
|
|
@ -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.
|
|
|
@ -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 />).
|
|
2
src/env.d.ts
vendored
|
@ -1 +1 @@
|
||||||
/// <reference path="../.astro/types.d.ts" />
|
/// <reference types="astro/client" />
|
|
@ -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>
|
|
|
@ -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>
|
|
84
src/layouts/Layout.astro
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
---
|
||||||
|
import { ArticleData } from "@extractus/article-extractor";
|
||||||
|
import "./reset.css";
|
||||||
|
import Footer from "../components/Footer.astro";
|
||||||
|
export interface Props {
|
||||||
|
meta: ArticleData
|
||||||
|
}
|
||||||
|
const { meta } = Astro.props;
|
||||||
|
const appTitle = `Cozy 🧸${meta.title && ` | ${meta.title}`}`;
|
||||||
|
---
|
||||||
|
|
||||||
|
<!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>
|
||||||
|
|
||||||
|
{
|
||||||
|
meta.url !== '/' ? (
|
||||||
|
<meta name="robots" content="noindex">
|
||||||
|
<meta name="googlebot" content="noindex">
|
||||||
|
) : (
|
||||||
|
<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." />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
<meta property="cozy:title" content={meta.title} />
|
||||||
|
<meta property="cozy:url" content={meta.url} />
|
||||||
|
<meta property="cozy:description" content={meta.description} />
|
||||||
|
<meta property="cozy:image" content={meta.image} />
|
||||||
|
<meta property="cozy:source" content={meta.source} />
|
||||||
|
<meta property="cozy:author" content={meta.author} />
|
||||||
|
<meta property="cozy:published" content={meta.published} />
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app-wrapper">
|
||||||
|
<slot />
|
||||||
|
<div id="main-content">
|
||||||
|
<div id="post-wrapper">
|
||||||
|
<slot name="post" />
|
||||||
|
</div>
|
||||||
|
<div id="library-wrapper">
|
||||||
|
<slot name="library" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Footer />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
#app-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 650px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
#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>
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -1,129 +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 { getCollection } from 'astro:content'
|
|
||||||
import { SITE_TITLE, SITE_DESCRIPTION } from '../../consts'
|
|
||||||
|
|
||||||
const posts = (await getCollection('blog')).sort(
|
|
||||||
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
|
|
||||||
)
|
|
||||||
---
|
|
||||||
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<BaseHead title={SITE_TITLE} description={SITE_DESCRIPTION} />
|
|
||||||
<style>
|
|
||||||
main {
|
|
||||||
width: 700px;
|
|
||||||
}
|
|
||||||
ul {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 2rem;
|
|
||||||
list-style-type: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
ul li * {
|
|
||||||
text-decoration: none;
|
|
||||||
transition: 0.2s ease;
|
|
||||||
}
|
|
||||||
.card {
|
|
||||||
border: 1px solid rgb(var(--gray));
|
|
||||||
border-radius: 12px;
|
|
||||||
width: 100%;
|
|
||||||
padding: 1em;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
text-align: center;
|
|
||||||
color: rgb(var(--black));
|
|
||||||
position: relative;
|
|
||||||
background-color: white;
|
|
||||||
|
|
||||||
& img {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 800px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .title {
|
|
||||||
font-size: 2.369rem;
|
|
||||||
margin: 1rem;
|
|
||||||
color: rgb(var(--black));
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .description {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
& h4 a::after {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
& h4 {
|
|
||||||
text-decoration: underline !important;
|
|
||||||
color: var(--accent);
|
|
||||||
text-decoration-thickness: 2px !important;
|
|
||||||
|
|
||||||
& a {
|
|
||||||
color: rgb(var(--black));
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover a {
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.date {
|
|
||||||
margin: 0;
|
|
||||||
color: rgb(var(--gray));
|
|
||||||
}
|
|
||||||
@media (max-width: 700px) {
|
|
||||||
ul {
|
|
||||||
gap: 0.5em;
|
|
||||||
}
|
|
||||||
ul li {
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<Header />
|
|
||||||
<main>
|
|
||||||
<section>
|
|
||||||
<ul>
|
|
||||||
{
|
|
||||||
posts.map((post) => (
|
|
||||||
<li class="card">
|
|
||||||
{/* {
|
|
||||||
post.data.heroImage
|
|
||||||
? <img width={700} height={360} src={post.data.heroImage} alt="" />
|
|
||||||
: <img width={700} height={360} src="/blog-placeholder-4.jpg" alt="" />
|
|
||||||
} */}
|
|
||||||
<small class="date">
|
|
||||||
<FormattedDate date={post.data.pubDate} />
|
|
||||||
</small>
|
|
||||||
<h4 class="title">
|
|
||||||
<a href={`/blog/${post.id}/`}>{post.data.title}</a>
|
|
||||||
</h4>
|
|
||||||
<p class="description">{post.data.description}</p>
|
|
||||||
</li>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
<Footer />
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,74 +1,43 @@
|
||||||
---
|
---
|
||||||
import { createClient, type RedisJSON } from 'redis'
|
import { ArticleData, extract } from "@extractus/article-extractor";
|
||||||
import { type 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 AddressBar from '../components/AddressBar.astro'
|
const params = Astro.url.searchParams;
|
||||||
import Post from '../components/Post.astro'
|
const url = params.get('url') || '';
|
||||||
import App from '../layouts/App.astro'
|
let article: ArticleData | null;
|
||||||
import Library from '../components/Library.astro'
|
let skipSave;
|
||||||
import Footer from '../components/Footer.astro'
|
|
||||||
|
|
||||||
// Initialize Redis client
|
const error = {
|
||||||
const client = createClient()
|
title: "Something is not right",
|
||||||
client.on('error', (err) => console.error('Redis Client Error', err))
|
content: "<p>The article extractor did not get any result.</p>",
|
||||||
await client.connect()
|
|
||||||
|
|
||||||
// Disable prerendering for dynamic content
|
|
||||||
export const prerender = false
|
|
||||||
|
|
||||||
// Get URL parameter from query string
|
|
||||||
let url = Astro.url.searchParams.get('url')
|
|
||||||
let article: ArticleData | null = { url: '/' }
|
|
||||||
|
|
||||||
// Handle redirect loops by extracting URL from nested parameters
|
|
||||||
while (url?.startsWith(Astro.url.origin)) {
|
|
||||||
try {
|
|
||||||
// Parse the URL to extract search parameters
|
|
||||||
const parsedUrl = new URL(url)
|
|
||||||
url = parsedUrl.searchParams.get('url')
|
|
||||||
} catch {
|
|
||||||
// If URL parsing fails, break the loop
|
|
||||||
console.error('Failed to parse URL:', url)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process article extraction only if a valid URL is provided
|
try {
|
||||||
if (url && url !== '/' && url !== '') {
|
article = await extract(url);
|
||||||
const cacheKey = 'cozy:url:' + url
|
if (!article ) {
|
||||||
|
article = error;
|
||||||
try {
|
skipSave = true;
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
article = error;
|
||||||
|
skipSave = true;
|
||||||
}
|
}
|
||||||
---
|
|
||||||
|
|
||||||
<App article={article}>
|
if (url === '') {
|
||||||
|
article = {
|
||||||
|
title: "Welcome to Cozy 🧸",
|
||||||
|
content: "<p>Enter a URL above to get started.</p>",
|
||||||
|
url: '/'
|
||||||
|
};
|
||||||
|
skipSave = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
---
|
||||||
|
<Layout meta={article}>
|
||||||
<AddressBar url={url} />
|
<AddressBar url={url} />
|
||||||
<div slot="post" id="router-outlet">
|
<Post slot="post" article={article} />
|
||||||
<Post article={article} />
|
<Library skipSave={skipSave} slot="library" postDivSelector="#post"/>
|
||||||
</div>
|
</Layout>
|
||||||
<Library slot="library" skipSave={article === null} />
|
|
||||||
<Footer slot="footer" />
|
|
||||||
</App>
|
|
||||||
|
|
|
@ -1,177 +0,0 @@
|
||||||
/*
|
|
||||||
The CSS in this style tag is based off of Bear Blog's default CSS.
|
|
||||||
https://github.com/HermanMartinus/bearblog/blob/297026a877bc2ab2b3bdfbd6b9f7961c350917dd/templates/styles/blog/default.css
|
|
||||||
License MIT: https://github.com/HermanMartinus/bearblog/blob/master/LICENSE.md
|
|
||||||
*/
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Atkinson';
|
|
||||||
src: url('/fonts/atkinson-regular.woff') format('woff');
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Atkinson';
|
|
||||||
src: url('/fonts/atkinson-bold.woff') format('woff');
|
|
||||||
font-weight: 700;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Atkinson', sans-serif;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
text-align: left;
|
|
||||||
background: linear-gradient(var(--gray-gradient)) no-repeat;
|
|
||||||
background-size: 100% 600px;
|
|
||||||
word-wrap: break-word;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
color: rgb(var(--gray-dark));
|
|
||||||
font-size: 20px;
|
|
||||||
line-height: 1.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
width: 700px;
|
|
||||||
max-width: calc(100% - 2em);
|
|
||||||
margin: auto;
|
|
||||||
padding: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4,
|
|
||||||
h5,
|
|
||||||
h6 {
|
|
||||||
margin: 0 0 0.5rem 0;
|
|
||||||
color: rgb(var(--black));
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 2.441em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 1.753em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.563em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h4 {
|
|
||||||
font-size: 1.35em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h5 {
|
|
||||||
font-size: 1.15em;
|
|
||||||
}
|
|
||||||
|
|
||||||
strong,
|
|
||||||
b {
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
p,
|
|
||||||
ul,
|
|
||||||
ol,
|
|
||||||
table,
|
|
||||||
pre.astro-code {
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose {
|
|
||||||
& p,
|
|
||||||
& ul,
|
|
||||||
& ol,
|
|
||||||
& table {
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
width: 100%;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
padding: 2px 5px;
|
|
||||||
background-color: rgb(var(--gray-light));
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
padding: 1.5em;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre > code {
|
|
||||||
all: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote {
|
|
||||||
border-left: 2px solid var(--accent);
|
|
||||||
padding: 0 0 0 1em;
|
|
||||||
margin: 0px;
|
|
||||||
font-size: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
|
||||||
border: none;
|
|
||||||
border-top: 1px solid rgb(var(--gray-light));
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 700px) {
|
|
||||||
body {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
padding: 1em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sr-only {
|
|
||||||
border: 0;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
position: absolute !important;
|
|
||||||
height: 1px;
|
|
||||||
width: 1px;
|
|
||||||
overflow: hidden;
|
|
||||||
/* IE6, IE7 - a 0 height clip, off to the bottom right of the visible 1px box */
|
|
||||||
clip: rect(1px 1px 1px 1px);
|
|
||||||
/* maybe deprecated but we need to support legacy browsers */
|
|
||||||
clip: rect(1px, 1px, 1px, 1px);
|
|
||||||
/* modern browsers, clip-path works inwards from each corner */
|
|
||||||
clip-path: inset(50%);
|
|
||||||
/* added line to stop words getting smushed together (as they go onto separate lines and some screen readers do not understand line feeds as a space */
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
|
@ -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%);
|
|
||||||
}
|
|
148
src/sw.mjs
|
@ -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: './',
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
|
@ -1,5 +1,23 @@
|
||||||
export function getPostCard(html: HTMLHtmlElement) {
|
export function getPostCard(html: HTMLHtmlElement) {
|
||||||
const { title, description, image, source, published } = getPostMeta(html)
|
const title =
|
||||||
|
html
|
||||||
|
.querySelector('meta[property="cozy:title"]')
|
||||||
|
?.getAttribute("content") ||
|
||||||
|
html.querySelector("title")?.innerHTML?.replace("Cozy 🧸 | ", "");
|
||||||
|
|
||||||
|
const description = html
|
||||||
|
.querySelector('meta[property="cozy:description"]')
|
||||||
|
?.getAttribute("content");
|
||||||
|
const image = html
|
||||||
|
.querySelector('meta[property="cozy:image"]')
|
||||||
|
?.getAttribute("content");
|
||||||
|
const source = html
|
||||||
|
.querySelector('meta[property="cozy:source"]')
|
||||||
|
?.getAttribute("content");
|
||||||
|
const published = html
|
||||||
|
.querySelector('meta[property="cozy:published"]')
|
||||||
|
?.getAttribute("content");
|
||||||
|
|
||||||
const postCard = `
|
const postCard = `
|
||||||
<div class="post-card">
|
<div class="post-card">
|
||||||
<div class="post-card__image">
|
<div class="post-card__image">
|
||||||
|
@ -17,109 +35,66 @@ 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(responseText, url, postDivSelector: string, preventPushState = false) {
|
||||||
|
const postDiv = document.querySelector(postDivSelector);
|
||||||
|
const html = document.createElement('html');
|
||||||
|
html.innerHTML = responseText;
|
||||||
|
const newPost = html.querySelector('body')?.querySelector('#post');
|
||||||
|
if (postDiv && newPost?.innerHTML) {
|
||||||
|
postDiv.innerHTML = newPost.innerHTML
|
||||||
|
const appUrl = document.getElementById('app-url') as HTMLInputElement;
|
||||||
|
const cozyUrl = html.querySelector('meta[property="cozy:url"]')?.getAttribute('content');
|
||||||
|
const homeBtn = document.querySelector<HTMLButtonElement>('#app-home');
|
||||||
|
const backBtn = document.querySelector<HTMLButtonElement>('#app-back');
|
||||||
|
const submitBtn = document.querySelector<HTMLButtonElement>('#app-submit');
|
||||||
|
|
||||||
export function renderPost(
|
|
||||||
responseText: string | null,
|
|
||||||
url,
|
|
||||||
postDivSelector: string,
|
|
||||||
preventPushState = false
|
|
||||||
) {
|
|
||||||
const postDiv = document.querySelector<HTMLDivElement>(`#${postDivSelector}`)
|
|
||||||
let postText = ''
|
|
||||||
let cozyUrl = '/'
|
|
||||||
let cozyTitle = 'Cozy'
|
|
||||||
if (responseText) {
|
|
||||||
const html = document.createElement('html')
|
|
||||||
html.innerHTML = responseText
|
|
||||||
const newPost = html.querySelector('body')?.querySelector('#post')
|
|
||||||
postText = newPost?.outerHTML || ''
|
|
||||||
cozyUrl =
|
|
||||||
html
|
|
||||||
.querySelector('meta[property="cozy:url"]')
|
|
||||||
?.getAttribute('content') ?? '/'
|
|
||||||
cozyTitle = `${getCozyTitle(html)} | Cozy`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (postDiv) {
|
const title = html.querySelector('meta[property="cozy:title"]')?.getAttribute('content');
|
||||||
postDiv.innerHTML = postText
|
document.title = `Cozy 🧸 | ${title}` || 'Cozy 🧸';
|
||||||
|
|
||||||
const appUrl = document.getElementById('app-url') as HTMLInputElement
|
if(cozyUrl !== '/') {
|
||||||
const backBtn = document.querySelector<HTMLButtonElement>('#app-back')
|
appUrl.value = cozyUrl || '';
|
||||||
const submitBtn = document.querySelector<HTMLButtonElement>('#submit')
|
backBtn?.removeAttribute('disabled');
|
||||||
if (cozyUrl !== '/') {
|
submitBtn?.removeAttribute('disabled');
|
||||||
appUrl.value = cozyUrl || ''
|
homeBtn?.removeAttribute('disabled');
|
||||||
backBtn?.removeAttribute('disabled')
|
|
||||||
submitBtn?.removeAttribute('disabled')
|
|
||||||
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`
|
homeBtn?.setAttribute('disabled', 'true');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!preventPushState) {
|
if(!preventPushState) {
|
||||||
window.history.pushState({ url }, '', url)
|
window.history.pushState({url}, '', url);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function getPostMeta(html: HTMLHtmlElement) {
|
|
||||||
const title = getCozyTitle(html)
|
|
||||||
const description = html
|
|
||||||
.querySelector('meta[property="cozy:description"]')
|
|
||||||
?.getAttribute('content')
|
|
||||||
const image = html
|
|
||||||
.querySelector('meta[property="cozy:image"]')
|
|
||||||
?.getAttribute('content')
|
|
||||||
const source = html
|
|
||||||
.querySelector('meta[property="cozy:source"]')
|
|
||||||
?.getAttribute('content')
|
|
||||||
const published = html
|
|
||||||
.querySelector('meta[property="cozy:published"]')
|
|
||||||
?.getAttribute('content')
|
|
||||||
|
|
||||||
return { title, description, image, source, published }
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCozyTitle(html: HTMLHtmlElement): string | undefined {
|
|
||||||
return (
|
|
||||||
html
|
|
||||||
.querySelector('meta[property="cozy:title"]')
|
|
||||||
?.getAttribute('content') ??
|
|
||||||
/**
|
|
||||||
* backwards compatibility for stuff before we implemented cozy:meta tags
|
|
||||||
* REMOVE ON V1 release
|
|
||||||
*/
|
|
||||||
html.querySelector('title')?.innerHTML?.replace('Cozy 🧸 | ', '')
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
import { parse, render, transform, walkSync } from 'ultrahtml'
|
|
||||||
import sanitize from 'ultrahtml/transformers/sanitize'
|
|
||||||
|
|
||||||
export async function cozify(html: string, baseUrl: string): Promise<string> {
|
|
||||||
// remove target="_blank" from links
|
|
||||||
const ast = parse(html)
|
|
||||||
walkSync(ast, (node) => {
|
|
||||||
if (node.name === 'a') {
|
|
||||||
node.attributes.href = `${baseUrl}?url=${node.attributes.href}`
|
|
||||||
node.attributes.prefetch = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const newHtml = await render(ast)
|
|
||||||
|
|
||||||
return transform(newHtml, [
|
|
||||||
sanitize({
|
|
||||||
dropElements: ['script'],
|
|
||||||
dropAttributes: {
|
|
||||||
target: ['a'],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
}
|
|
|
@ -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"')
|
|
||||||
})
|
|
||||||
})
|
|