chore: format code

This commit is contained in:
Ayo Ayco 2024-12-26 00:10:13 +01:00
parent d21e743569
commit 99315e639a
28 changed files with 1059 additions and 1027 deletions

View file

@ -37,7 +37,7 @@ export default [
parser: astroParser,
parserOptions: {
parser: tseslint.parser,
}
},
},
},
]

View file

@ -1,16 +1,15 @@
---
import Icon from 'astro-iconify'
import {VERSION} from '../consts';
import { VERSION } from '../consts'
---
<footer>
<section>
Remove distractions. Save for later.
</section>
<section>Remove distractions. Save for later.</section>
<section class="attribution">
<a href="/blog/01-building-a-cozy-web/">Hand-crafted</a> with <Icon name="line-md:heart" /> by <a href="https://ayo.ayco.io">Ayo Ayco</a>
<a href="/blog/01-building-a-cozy-web/">Hand-crafted</a> with <Icon
name="line-md:heart"
/> by <a href="https://ayo.ayco.io">Ayo Ayco</a>
<br />
<a href="/blog">Blog</a> •
<a href="https://github.com/ayoayco/cozy">GitHub</a> •

View file

@ -1,12 +1,12 @@
---
const { default: innerHTML } = await import(`/public/jumbotron.svg?raw`);
const { default: innerHTML } = await import(`/public/jumbotron.svg?raw`)
---
<header id="jumbotron"><a href="/">
<header id="jumbotron">
<a href="/">
<Fragment set:html={innerHTML} />
</a></header>
</a>
</header>
<style>
#jumbotron {
@ -19,7 +19,7 @@ const { default: innerHTML } = await import(`/public/jumbotron.svg?raw`);
}
&:hover {
filter: var(--svg-filter-accent)
filter: var(--svg-filter-accent);
}
}
</style>

View file

@ -1,6 +1,6 @@
---
export interface Props {
skipSave?: boolean;
skipSave?: boolean
}
---
@ -10,25 +10,25 @@ export interface Props {
</div>
<script>
import { getPostCard, renderPost } from "../utils/library";
import { cozify } from "../utils/sanitizer";
const cache = await caches.open("cozy-reader");
const baseUrl = window.location.origin;
let url = new URL(window.location.href);
import { getPostCard, renderPost } from '../utils/library'
import { cozify } from '../utils/sanitizer'
const cache = await caches.open('cozy-reader')
const baseUrl = window.location.origin
let url = new URL(window.location.href)
// only cached unencoded url param
const urlParam = url.searchParams.get("url");
const urlParam = url.searchParams.get('url')
if (urlParam) {
url = new URL(`${url.origin}/?url=${urlParam}`);
url = new URL(`${url.origin}/?url=${urlParam}`)
}
const preferencesEl = document.querySelector(
"[data-preferences]"
) as HTMLElement;
const preferencesStr = preferencesEl?.dataset.preferences ?? "";
const { skipSave } = JSON.parse(preferencesStr);
'[data-preferences]'
) as HTMLElement
const preferencesStr = preferencesEl?.dataset.preferences ?? ''
const { skipSave } = JSON.parse(preferencesStr)
const routerOutlet = "router-outlet";
const includesAppURL = urlParam?.includes(baseUrl) ?? false;
const routerOutlet = 'router-outlet'
const includesAppURL = urlParam?.includes(baseUrl) ?? false
try {
if (
@ -36,110 +36,110 @@ export interface Props {
!skipSave &&
!includesAppURL
) {
console.info("adding one to cache", {
context: "cozy-reader",
console.info('adding one to cache', {
context: 'cozy-reader',
data: url,
});
await cache.add(url);
})
await cache.add(url)
}
} catch (error) {
console.error("ERR", { context: "cozy-reader", data: error });
console.error('ERR', { context: 'cozy-reader', data: error })
}
const cachedRequests = (await cache.keys()).filter((request) => {
const urlObj = new URL(request.url);
const urlParam = urlObj.searchParams.get("url");
const urlObj = new URL(request.url)
const urlParam = urlObj.searchParams.get('url')
return (
urlObj.search !== "" &&
urlObj.search !== '' &&
!urlParam?.startsWith(baseUrl) &&
urlParam !== "" &&
urlParam !== "null"
);
});
urlParam !== '' &&
urlParam !== 'null'
)
})
if (cachedRequests?.length && routerOutlet !== null) {
const list = document.querySelector("#post-list");
const list = document.querySelector('#post-list')
const heading = document.querySelector(
"#library span#heading"
) as HTMLHeadingElement;
heading.innerHTML = "History";
'#library span#heading'
) as HTMLHeadingElement
heading.innerHTML = 'History'
cachedRequests.reverse().forEach(async (request) => {
const { url } = request;
const link = document.createElement("a");
const { url } = request
const link = document.createElement('a')
let responseText;
const fullResponse = await cache.match(url);
let responseText
const fullResponse = await cache.match(url)
if (
!fullResponse &&
url.slice(0, url.length - 1) !== baseUrl &&
!skipSave &&
!includesAppURL
) {
console.info("updating cached", { context: "cozy-reader", data: url });
await cache.add(url);
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;
responseText = data
const cleanedResponse = await cozify(responseText, baseUrl)
const html = document.createElement('html')
html.innerHTML = cleanedResponse
const title = html
.querySelector('meta[property="cozy:title"]')
?.getAttribute("content");
if (title === "Something is not right") {
cache.delete(url);
return; // temporary fix for deleting cached errors
?.getAttribute('content')
if (title === 'Something is not right') {
cache.delete(url)
return // temporary fix for deleting cached errors
}
const postCard = getPostCard(html);
link.innerHTML = postCard;
const postCard = getPostCard(html)
link.innerHTML = postCard
link.href = url;
link.href = url
link.onclick = async (e) => {
e.preventDefault();
localStorage.setItem("scrollPosition", window.scrollY.toString());
scrollTo(0, 0);
console.info("using cached response", {
context: "cozy-reader",
e.preventDefault()
localStorage.setItem('scrollPosition', window.scrollY.toString())
scrollTo(0, 0)
console.info('using cached response', {
context: 'cozy-reader',
data: url,
});
renderPost(cleanedResponse, url, routerOutlet);
};
const item = document.createElement("li");
item.appendChild(link);
list?.appendChild(item);
});
});
})
renderPost(cleanedResponse, url, routerOutlet)
}
const item = document.createElement('li')
item.appendChild(link)
list?.appendChild(item)
})
})
window.addEventListener("popstate", async (data) => {
let url = data.state?.url;
let isHome = false;
window.addEventListener('popstate', async (data) => {
let url = data.state?.url
let isHome = false
if (!url) {
url = window.location.href;
isHome = true;
url = window.location.href
isHome = true
} else {
// 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) => {
const responseText = data;
const cleanedResponse = await cozify(responseText, baseUrl);
console.info("using cached response", {
context: "cozy-reader",
const responseText = data
const cleanedResponse = await cozify(responseText, baseUrl)
console.info('using cached response', {
context: 'cozy-reader',
data: url,
});
renderPost(cleanedResponse, url, routerOutlet, true);
})
renderPost(cleanedResponse, url, routerOutlet, true)
if (isHome) {
const scrollPosition = localStorage.getItem("scrollPosition");
scrollTo(0, scrollPosition ? parseInt(scrollPosition) : 0);
const scrollPosition = localStorage.getItem('scrollPosition')
scrollTo(0, scrollPosition ? parseInt(scrollPosition) : 0)
}
});
});
})
})
}
</script>

View file

@ -1,27 +1,26 @@
---
import { ArticleData } from "@extractus/article-extractor";
import { cozify } from "../utils/sanitizer"
import { ArticleData } from '@extractus/article-extractor'
import { cozify } from '../utils/sanitizer'
export interface Props {
article: ArticleData | null;
article: ArticleData | null
}
const error: ArticleData = {
title: 'Something is not right',
content: '<p>The article extractor did not get any information.</p>',
}
let { article } = Astro.props;
article ??= error;
let { article } = Astro.props
article ??= error
const datePublished =
article?.published && new Date(article.published).toDateString();
article?.published && new Date(article.published).toDateString()
const cleanContent = await cozify(article.content ?? '', Astro.url.origin)
---
<!--
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.title && <h1 class="title">{article.title}</h1>}
@ -33,22 +32,27 @@ const cleanContent = await cozify(article.content ?? '', Astro.url.origin)
)}
<content set:html={cleanContent} />
</article>
)
}
<style>
#post {
h1.title {
font-size: xx-large;
margin: 0;
}
h1, h2, h3, h4, h5, h6 {
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.2;
}
.source, .publish-info {
.source,
.publish-info {
font-size: smaller;
color: #555;
}
@ -68,7 +72,10 @@ const cleanContent = await cozify(article.content ?? '', Astro.url.origin)
}
content {
p, table, ul, img {
p,
table,
ul,
img {
margin: 1em 0 !important;
font-size: 20px;
}
@ -76,7 +83,8 @@ const cleanContent = await cozify(article.content ?? '', Astro.url.origin)
table {
border-collapse: collapse;
td, th {
td,
th {
border: 1px solid #ccc;
padding: 0.5em;
}
@ -92,11 +100,13 @@ const cleanContent = await cozify(article.content ?? '', Astro.url.origin)
}
@media (max-width: 600px) {
p, table, ul, img {
p,
table,
ul,
img {
font-size: 16px;
}
}
}
}
</style>

View file

@ -1,12 +1,12 @@
---
import Icon from "astro-iconify";
import Icon from 'astro-iconify'
export interface Props {
url: string | null;
url: string | null
}
const placeholder = "Type the article URL here";
const { url } = Astro.props;
const placeholder = 'Type the article URL here'
const { url } = Astro.props
---
<div id="address-bar">
@ -18,7 +18,7 @@ const { url } = Astro.props;
type="url"
id="app-url"
name="url"
value={url ?? ""}
value={url ?? ''}
placeholder={placeholder}
required
/>
@ -39,7 +39,6 @@ const { url } = Astro.props;
>
<Icon name="mdi:home" />
</a>
</form>
</div>
@ -49,9 +48,7 @@ const { url } = Astro.props;
position: relative;
}
form:has(
input[type="url"]:focus
) {
form:has(input[type='url']:focus) {
border-color: var(--accent);
box-shadow: 0 1px 10px 0px var(--accent);
}
@ -66,11 +63,11 @@ const { url } = Astro.props;
box-shadow: 0 1px 3px 1px rgb(var(--gray-light));
display: flex;
input[type="url"]:focus {
input[type='url']:focus {
outline: none;
}
input[type="url"] {
input[type='url'] {
flex: 3;
border: 0px;
border-radius: 30px;
@ -131,7 +128,7 @@ const { url } = Astro.props;
color: blue !important;
}
.btn[disabled="true"] svg {
.btn[disabled='true'] svg {
color: rgb(var(--gray-light)) !important;
cursor: default !important;
}

View file

@ -1,21 +1,25 @@
---
import '../../styles/reset.css';
import '../../styles/variables.css';
import '../../styles/blog.css';
import { SITE_TITLE, SITE_AUTHOR, SITE_DESCRIPTION } from '../../consts';
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;
image?: string;
title: string
description: string
isArticle?: boolean
image?: string
}
let {isArticle = false, title, description = 'default description', image = '/cozy.jpg' } = Astro.props;
let {
isArticle = false,
title,
description = 'default description',
image = '/cozy.jpg',
} = Astro.props
description = title === SITE_TITLE
? SITE_DESCRIPTION
: `${description} • ${SITE_TITLE}`
description =
title === SITE_TITLE ? SITE_DESCRIPTION : `${description} • ${SITE_TITLE}`
---
<!-- Global Metadata -->
@ -26,8 +30,20 @@ description = title === SITE_TITLE
<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 />
<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>
@ -36,9 +52,11 @@ description = title === SITE_TITLE
<!-- Open Graph / Facebook -->
{
isArticle
? <meta property="og:type" content="article" />
: <meta property="og:type" content="website" />
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} />
@ -46,5 +64,3 @@ description = title === SITE_TITLE
<meta property="og:image" content={new URL(image, Astro.url)} />
<meta property="og:site_name" content={SITE_TITLE} />
<meta property="article:author" content={SITE_AUTHOR} />

View file

@ -1,11 +1,20 @@
---
import { SITE_AUTHOR, SITE_AUTHOR_EMAIL, SITE_AUTHOR_MASTODON, SITE_PROJECT_REPO } from "../../consts";
const today = new Date();
import {
SITE_AUTHOR,
SITE_AUTHOR_EMAIL,
SITE_AUTHOR_MASTODON,
SITE_PROJECT_REPO,
} from '../../consts'
const today = new Date()
---
<footer>
<p>&copy; {today.getFullYear()} {SITE_AUTHOR}. All rights reserved.</p>
<p>Want to get in touch? Send a mail to <a href={`mailto:${SITE_AUTHOR_EMAIL}`}>Cozy at ayco.io</a>.</p>
<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>
@ -23,7 +32,12 @@ const today = new Date();
</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"
<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"

View file

@ -1,9 +1,9 @@
---
interface Props {
date: Date;
date: Date
}
const { date } = Astro.props;
const { date } = Astro.props
---
<time datetime={date.toISOString()}>

View file

@ -1,5 +1,10 @@
---
import { SITE_AUTHOR_MASTODON, SITE_DESCRIPTION, SITE_PROJECT_REPO, SITE_TITLE } from '../../consts';
import {
SITE_AUTHOR_MASTODON,
SITE_DESCRIPTION,
SITE_PROJECT_REPO,
SITE_TITLE,
} from '../../consts'
---
<header>
@ -69,7 +74,6 @@ import { SITE_AUTHOR_MASTODON, SITE_DESCRIPTION, SITE_PROJECT_REPO, SITE_TITLE }
padding: 1px 0.5em 0;
transition: 0.2s ease;
}
}
nav .social-links a:hover {

View file

@ -1,14 +1,14 @@
---
import type { HTMLAttributes } from 'astro/types';
import type { HTMLAttributes } from 'astro/types'
type Props = HTMLAttributes<'a'>;
type Props = HTMLAttributes<'a'>
const { href, class: className, ...props } = Astro.props;
const { href, class: className, ...props } = Astro.props
const { pathname } = Astro.url;
const { pathname } = Astro.url
// eslint-disable-next-line no-useless-escape
const subpath = pathname.match(/[^\/]+/g);
const isActive = href === pathname || href === '/' + subpath?.[0];
const subpath = pathname.match(/[^\/]+/g)
const isActive = href === pathname || href === '/' + subpath?.[0]
---
<a href={href} class:list={[className, { active: isActive }]} {...props}>

View file

@ -1,9 +1,9 @@
export const SITE_TITLE = 'Cozy Blog';
export const SITE_AUTHOR = 'Ayo Ayco';
export const SITE_AUTHOR_URL = 'https://ayo.ayco.io';
export const SITE_AUTHOR_EMAIL = 'cozy@ayco.io';
export const SITE_AUTHOR_MASTODON = 'https://social.ayco.io/@ayo';
export const SITE_PROJECT_REPO = 'https://github.com/ayoayco/Cozy';
export const SITE_DESCRIPTION = 'The Web is Yours.';
export const 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 = 'Yummy-Ydrasil';
export const VERSION = 'Yummy-Ydrasil'

View file

@ -30,6 +30,7 @@ The project and the road map for features are all public on my [GitHub](https://
Right now, it successfully extracts the content and delivers a clean page to your browser.
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
@ -50,7 +51,7 @@ javascript:(function(){ window.open('https://cozy.pub/?url=%27 + window.location
This is possible on all major browsers, including Safari on iOS (where I personally use this often). Some screenshots:
| Firefox | Chrome |
| --- | --- |
| ---------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| ![Screenshot from 2023-05-13 08-31-41](https://github.com/ayoayco/cozy-reader/assets/4262489/9b296d4f-2722-483a-bbc2-431c6b2ae996) | ![Screenshot from 2023-05-12 23-32-08](https://github.com/ayoayco/cozy-reader/assets/4262489/144b74f8-3949-46b9-849c-351e4af0ac12) |
## Join the Project!

View file

@ -1,4 +1,4 @@
import { defineCollection, z } from 'astro:content';
import { defineCollection, z } from 'astro:content'
const blog = defineCollection({
type: 'content',
@ -11,6 +11,6 @@ const blog = defineCollection({
updatedDate: z.coerce.date().optional(),
heroImage: z.string().optional(),
}),
});
})
export const collections = { blog };
export const collections = { blog }

View file

@ -12,13 +12,10 @@ Since then, I've been using Cozy almost every time I read an article online. I h
You visit a news website, for example, and you just know the content are mostly just a bait
Browsers are not helping. AI
Having a web page let's me skip all the noise that plague almost all modern websites
[Astro's on-demand rendering](https://docs.astro.build/en/guides/server-side-rendering/) and [JavaScript's Cache API](https://developer.mozilla.org/en-US/docs/Web/API/Cache)
The goal for this site is to have a place where I publish about the new features of the [web app](/) and the web development techniques used to achieve them.

View file

@ -1,14 +1,14 @@
---
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";
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"];
type Props = CollectionEntry<'blog'>['data']
const { title, description, pubDate, updatedDate, heroImage } = Astro.props;
const { title, description, pubDate, updatedDate, heroImage } = Astro.props
---
<html lang="en">
@ -64,7 +64,6 @@ const { title, description, pubDate, updatedDate, heroImage } = Astro.props;
color: var(--accent);
box-shadow: 0 2px 8px var(--accent);
}
}
}
@ -138,16 +137,7 @@ const { title, description, pubDate, updatedDate, heroImage } = Astro.props;
<main>
<article>
<div class="hero-image">
{
heroImage && (
<img
width={700}
height={510}
src={heroImage}
alt=""
/>
)
}
{heroImage && <img width={700} height={510} src={heroImage} alt="" />}
</div>
<div class="prose">
<div class="title">
@ -163,7 +153,12 @@ const { title, description, pubDate, updatedDate, heroImage } = Astro.props;
</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>
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 />

View file

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

View file

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

View file

@ -1,14 +1,14 @@
---
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';
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(),
);
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
)
---
<!doctype html>
@ -115,13 +115,9 @@ const posts = (await getCollection('blog')).sort(
<FormattedDate date={post.data.pubDate} />
</small>
<h4 class="title">
<a href={`/blog/${post.slug}/`}>
{post.data.title}
</a>
<a href={`/blog/${post.slug}/`}>{post.data.title}</a>
</h4>
<p class="description">
{post.data.description}
</p>
<p class="description">{post.data.description}</p>
</li>
))
}

View file

@ -1,28 +1,28 @@
---
import { ArticleData, extract } from "@extractus/article-extractor";
import SimpleAddressBar from "../components/SimpleAddressBar.astro";
import Post from "../components/Post.astro";
import App from "../layouts/App.astro";
import Library from "../components/Library.astro";
import Footer from "../components/Footer.astro";
import { ArticleData, extract } from '@extractus/article-extractor'
import SimpleAddressBar from '../components/SimpleAddressBar.astro'
import Post from '../components/Post.astro'
import App from '../layouts/App.astro'
import Library from '../components/Library.astro'
import Footer from '../components/Footer.astro'
export const prerender = false;
export const prerender = false
let url = Astro.url.searchParams.get('url');
let article: ArticleData | null = {url: '/'};
let url = Astro.url.searchParams.get('url')
let article: ArticleData | null = { url: '/' }
while (url?.startsWith(Astro.url.origin)) {
url = new URL(url).searchParams.get('url');
url = new URL(url).searchParams.get('url')
}
if (url)
try {
article = await extract(url);
article = await extract(url)
} catch {
article = null;
article = null
}
---
<App article={article}>
<SimpleAddressBar url={url} />
<div slot="post" id="router-outlet">

View file

@ -32,7 +32,6 @@ body {
color: rgb(var(--gray-dark));
font-size: 20px;
line-height: 1.7;
}
main {

View file

@ -1,13 +1,13 @@
:root {
--accent: #3054bf;
--svg-filter-accent: invert(25%) sepia(86%) saturate(1533%) hue-rotate(210deg) brightness(91%) contrast(90%);
--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%);
--box-shadow: 0 2px 6px rgba(var(--gray), 25%),
0 8px 24px rgba(var(--gray), 33%), 0 16px 32px rgba(var(--gray), 33%);
}

View file

@ -6,86 +6,88 @@
const cacheName = `${__prefix ?? 'app'}-v${__version ?? '000'}`
const cleanOldCaches = async () => {
const allowCacheNames = ['cozy-reader', cacheName];
const allCaches = await caches.keys();
allCaches.forEach(key => {
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);
console.info('Deleting old cache', key)
caches.delete(key)
}
});
})
}
const addResourcesToCache = async (resources) => {
const cache = await caches.open(cacheName);
const cache = await caches.open(cacheName)
console.info('adding resources to cache...', resources)
try {
await cache.addAll(resources);
await cache.addAll(resources)
} catch (error) {
console.error('failed to add resources to cache; make sure requests exists and that there are no duplicates', {
console.error(
'failed to add resources to cache; make sure requests exists and that there are no duplicates',
{
resources,
error
})
error,
}
)
}
}
};
const putInCache = async (request, response) => {
const cache = await caches.open(cacheName);
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 => {
cache.keys().then((keys) => {
if (keys.includes(request)) {
cache.delete(request);
cache.delete(request)
}
});
})
cache.put(request, response);
cache.put(request, response)
}
}
};
const cacheAndRevalidate = async ({ request, fallbackUrl }) => {
const cache = await caches.open(cacheName);
const cache = await caches.open(cacheName)
// Try get the resource from the cache
const responseFromCache = await cache.match(request);
const responseFromCache = await cache.match(request)
if (responseFromCache) {
console.info('using cached response...', responseFromCache.url);
console.info('using cached response...', responseFromCache.url)
// get network response for revalidation of cached assets
fetch(request.clone()).then((responseFromNetwork) => {
fetch(request.clone())
.then((responseFromNetwork) => {
if (responseFromNetwork) {
console.info('fetched updated resource...', responseFromNetwork.url)
putInCache(request, responseFromNetwork.clone());
putInCache(request, responseFromNetwork.clone())
}
}).catch((error) => {
})
.catch((error) => {
console.info('failed to fetch updated resource', error)
});
})
return responseFromCache;
return responseFromCache
}
try {
// Try to get the resource from the network for 5 seconds
const responseFromNetwork = await fetch(request.clone());
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());
putInCache(request, responseFromNetwork.clone())
console.info('using network response', responseFromNetwork.url)
return responseFromNetwork;
return responseFromNetwork
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
// Try the fallback
const fallbackResponse = await cache.match(fallbackUrl);
const fallbackResponse = await cache.match(fallbackUrl)
if (fallbackResponse) {
console.info('using fallback cached response...', fallbackResponse.url)
return fallbackResponse;
return fallbackResponse
}
// when even the fallback response is not available,
@ -94,33 +96,30 @@ const cacheAndRevalidate = async ({ request, fallbackUrl }) => {
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();
});
cleanOldCaches()
})
self.addEventListener('install', (event) => {
console.info('installing service worker...')
self.skipWaiting(); // go straight to activate
self.skipWaiting() // go straight to activate
event.waitUntil(
addResourcesToCache(__assets ?? [])
);
});
event.waitUntil(addResourcesToCache(__assets ?? []))
})
self.addEventListener('fetch', (event) => {
console.info('fetch happened', {data: event});
console.info('fetch happened', { data: event })
event.respondWith(
cacheAndRevalidate({
request: event.request,
fallbackUrl: './',
})
);
});
)
})

View file

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

View file

@ -2,7 +2,6 @@ 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) => {
@ -12,15 +11,14 @@ export async function cozify(html: string, baseUrl: string): Promise<string> {
}
})
const newHtml = await render(ast);
const newHtml = await render(ast)
return transform(newHtml, [
sanitize({
dropElements: ['script'],
dropAttributes: {
target: ['a']
}
})
target: ['a'],
},
}),
])
}

View file

@ -1,31 +1,31 @@
import { describe, expect, test } from "vitest";
import { cozify } from "../src/utils/sanitizer";
import { describe, expect, test } from 'vitest'
import { cozify } from '../src/utils/sanitizer'
describe("cozify()", async () => {
const baseUrl = "https://cozy.pub";
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 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 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 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"');
});
});
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"')
})
})