chore: format code
This commit is contained in:
parent
d21e743569
commit
99315e639a
28 changed files with 1059 additions and 1027 deletions
|
@ -37,7 +37,7 @@ export default [
|
||||||
parser: astroParser,
|
parser: astroParser,
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
parser: tseslint.parser,
|
parser: tseslint.parser,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
---
|
---
|
||||||
import Icon from 'astro-iconify'
|
import Icon from 'astro-iconify'
|
||||||
import {VERSION} from '../consts';
|
import { VERSION } from '../consts'
|
||||||
---
|
---
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<section>
|
<section>Remove distractions. Save for later.</section>
|
||||||
Remove distractions. Save for later.
|
|
||||||
</section>
|
|
||||||
|
|
||||||
|
|
||||||
<section class="attribution">
|
<section class="attribution">
|
||||||
<a href="/blog/01-building-a-cozy-web/">Hand-crafted</a> with <Icon 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 />
|
<br />
|
||||||
<a href="/blog">Blog</a> •
|
<a href="/blog">Blog</a> •
|
||||||
<a href="https://github.com/ayoayco/cozy">GitHub</a> •
|
<a href="https://github.com/ayoayco/cozy">GitHub</a> •
|
||||||
|
|
|
@ -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} />
|
<Fragment set:html={innerHTML} />
|
||||||
|
</a>
|
||||||
</a></header>
|
</header>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#jumbotron {
|
#jumbotron {
|
||||||
|
@ -19,7 +19,7 @@ const { default: innerHTML } = await import(`/public/jumbotron.svg?raw`);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
filter: var(--svg-filter-accent)
|
filter: var(--svg-filter-accent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
export interface Props {
|
export interface Props {
|
||||||
skipSave?: boolean;
|
skipSave?: boolean
|
||||||
}
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -10,25 +10,25 @@ export interface Props {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getPostCard, renderPost } from "../utils/library";
|
import { getPostCard, renderPost } from '../utils/library'
|
||||||
import { cozify } from "../utils/sanitizer";
|
import { cozify } from '../utils/sanitizer'
|
||||||
const cache = await caches.open("cozy-reader");
|
const cache = await caches.open('cozy-reader')
|
||||||
const baseUrl = window.location.origin;
|
const baseUrl = window.location.origin
|
||||||
let url = new URL(window.location.href);
|
let url = new URL(window.location.href)
|
||||||
// only cached unencoded url param
|
// only cached unencoded url param
|
||||||
const urlParam = url.searchParams.get("url");
|
const urlParam = url.searchParams.get('url')
|
||||||
if (urlParam) {
|
if (urlParam) {
|
||||||
url = new URL(`${url.origin}/?url=${urlParam}`);
|
url = new URL(`${url.origin}/?url=${urlParam}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const preferencesEl = document.querySelector(
|
const preferencesEl = document.querySelector(
|
||||||
"[data-preferences]"
|
'[data-preferences]'
|
||||||
) as HTMLElement;
|
) as HTMLElement
|
||||||
const preferencesStr = preferencesEl?.dataset.preferences ?? "";
|
const preferencesStr = preferencesEl?.dataset.preferences ?? ''
|
||||||
const { skipSave } = JSON.parse(preferencesStr);
|
const { skipSave } = JSON.parse(preferencesStr)
|
||||||
|
|
||||||
const routerOutlet = "router-outlet";
|
const routerOutlet = 'router-outlet'
|
||||||
const includesAppURL = urlParam?.includes(baseUrl) ?? false;
|
const includesAppURL = urlParam?.includes(baseUrl) ?? false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (
|
if (
|
||||||
|
@ -36,110 +36,110 @@ export interface Props {
|
||||||
!skipSave &&
|
!skipSave &&
|
||||||
!includesAppURL
|
!includesAppURL
|
||||||
) {
|
) {
|
||||||
console.info("adding one to cache", {
|
console.info('adding one to cache', {
|
||||||
context: "cozy-reader",
|
context: 'cozy-reader',
|
||||||
data: url,
|
data: url,
|
||||||
});
|
})
|
||||||
await cache.add(url);
|
await cache.add(url)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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 cachedRequests = (await cache.keys()).filter((request) => {
|
||||||
const urlObj = new URL(request.url);
|
const urlObj = new URL(request.url)
|
||||||
const urlParam = urlObj.searchParams.get("url");
|
const urlParam = urlObj.searchParams.get('url')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
urlObj.search !== "" &&
|
urlObj.search !== '' &&
|
||||||
!urlParam?.startsWith(baseUrl) &&
|
!urlParam?.startsWith(baseUrl) &&
|
||||||
urlParam !== "" &&
|
urlParam !== '' &&
|
||||||
urlParam !== "null"
|
urlParam !== 'null'
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
|
|
||||||
if (cachedRequests?.length && routerOutlet !== null) {
|
if (cachedRequests?.length && routerOutlet !== null) {
|
||||||
const list = document.querySelector("#post-list");
|
const list = document.querySelector('#post-list')
|
||||||
const heading = document.querySelector(
|
const heading = document.querySelector(
|
||||||
"#library span#heading"
|
'#library span#heading'
|
||||||
) as HTMLHeadingElement;
|
) as HTMLHeadingElement
|
||||||
heading.innerHTML = "History";
|
heading.innerHTML = 'History'
|
||||||
|
|
||||||
cachedRequests.reverse().forEach(async (request) => {
|
cachedRequests.reverse().forEach(async (request) => {
|
||||||
const { url } = request;
|
const { url } = request
|
||||||
const link = document.createElement("a");
|
const link = document.createElement('a')
|
||||||
|
|
||||||
let responseText;
|
let responseText
|
||||||
const fullResponse = await cache.match(url);
|
const fullResponse = await cache.match(url)
|
||||||
if (
|
if (
|
||||||
!fullResponse &&
|
!fullResponse &&
|
||||||
url.slice(0, url.length - 1) !== baseUrl &&
|
url.slice(0, url.length - 1) !== baseUrl &&
|
||||||
!skipSave &&
|
!skipSave &&
|
||||||
!includesAppURL
|
!includesAppURL
|
||||||
) {
|
) {
|
||||||
console.info("updating cached", { context: "cozy-reader", data: url });
|
console.info('updating cached', { context: 'cozy-reader', data: url })
|
||||||
await cache.add(url);
|
await cache.add(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
fullResponse?.text().then(async (data) => {
|
fullResponse?.text().then(async (data) => {
|
||||||
responseText = data;
|
responseText = data
|
||||||
const cleanedResponse = await cozify(responseText, baseUrl);
|
const cleanedResponse = await cozify(responseText, baseUrl)
|
||||||
const html = document.createElement("html");
|
const html = document.createElement('html')
|
||||||
html.innerHTML = cleanedResponse;
|
html.innerHTML = cleanedResponse
|
||||||
const title = html
|
const title = html
|
||||||
.querySelector('meta[property="cozy:title"]')
|
.querySelector('meta[property="cozy:title"]')
|
||||||
?.getAttribute("content");
|
?.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", {
|
console.info('using cached response', {
|
||||||
context: "cozy-reader",
|
context: 'cozy-reader',
|
||||||
data: url,
|
data: url,
|
||||||
});
|
})
|
||||||
renderPost(cleanedResponse, url, routerOutlet);
|
renderPost(cleanedResponse, url, routerOutlet)
|
||||||
};
|
}
|
||||||
const item = document.createElement("li");
|
const item = document.createElement('li')
|
||||||
item.appendChild(link);
|
item.appendChild(link)
|
||||||
list?.appendChild(item);
|
list?.appendChild(item)
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
|
||||||
window.addEventListener("popstate", async (data) => {
|
window.addEventListener('popstate', async (data) => {
|
||||||
let url = data.state?.url;
|
let url = data.state?.url
|
||||||
let isHome = false;
|
let isHome = false
|
||||||
|
|
||||||
if (!url) {
|
if (!url) {
|
||||||
url = window.location.href;
|
url = window.location.href
|
||||||
isHome = true;
|
isHome = true
|
||||||
} else {
|
} else {
|
||||||
// replace scrollPosition
|
// replace scrollPosition
|
||||||
localStorage.setItem("scrollPosition", window.scrollY.toString());
|
localStorage.setItem('scrollPosition', window.scrollY.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullResponse = await cache.match(url);
|
const fullResponse = await cache.match(url)
|
||||||
fullResponse?.text().then(async (data) => {
|
fullResponse?.text().then(async (data) => {
|
||||||
const responseText = data;
|
const responseText = data
|
||||||
const cleanedResponse = await cozify(responseText, baseUrl);
|
const cleanedResponse = await cozify(responseText, baseUrl)
|
||||||
console.info("using cached response", {
|
console.info('using cached response', {
|
||||||
context: "cozy-reader",
|
context: 'cozy-reader',
|
||||||
data: url,
|
data: url,
|
||||||
});
|
})
|
||||||
renderPost(cleanedResponse, url, routerOutlet, true);
|
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>
|
||||||
|
|
||||||
|
|
|
@ -1,27 +1,26 @@
|
||||||
---
|
---
|
||||||
import { ArticleData } from "@extractus/article-extractor";
|
import { ArticleData } from '@extractus/article-extractor'
|
||||||
import { cozify } from "../utils/sanitizer"
|
import { cozify } from '../utils/sanitizer'
|
||||||
export interface Props {
|
export interface Props {
|
||||||
article: ArticleData | null;
|
article: ArticleData | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const error: ArticleData = {
|
const error: ArticleData = {
|
||||||
title: 'Something is not right',
|
title: 'Something is not right',
|
||||||
content: '<p>The article extractor did not get any information.</p>',
|
content: '<p>The article extractor did not get any information.</p>',
|
||||||
}
|
}
|
||||||
let { article } = Astro.props;
|
let { article } = Astro.props
|
||||||
article ??= error;
|
article ??= error
|
||||||
const datePublished =
|
const datePublished =
|
||||||
article?.published && new Date(article.published).toDateString();
|
article?.published && new Date(article.published).toDateString()
|
||||||
|
|
||||||
const cleanContent = await cozify(article.content ?? '', Astro.url.origin)
|
const cleanContent = await cozify(article.content ?? '', Astro.url.origin)
|
||||||
---
|
---
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Changing anything inside the #post div can cause a difference in the cached version of the post in users' devices. For this reason, we should avoid changing the HTML and instead do it with CSS when possible.
|
Changing anything inside the #post div can cause a difference in the cached version of the post in users' devices. For this reason, we should avoid changing the HTML and instead do it with CSS when possible.
|
||||||
-->
|
-->{
|
||||||
{
|
article && article.url !== '/' && (
|
||||||
article && article.url !== '/' &&
|
|
||||||
<article id="post">
|
<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>}
|
||||||
|
@ -33,22 +32,27 @@ const cleanContent = await cozify(article.content ?? '', Astro.url.origin)
|
||||||
)}
|
)}
|
||||||
<content set:html={cleanContent} />
|
<content set:html={cleanContent} />
|
||||||
</article>
|
</article>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#post {
|
#post {
|
||||||
|
|
||||||
h1.title {
|
h1.title {
|
||||||
font-size: xx-large;
|
font-size: xx-large;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.source, .publish-info {
|
.source,
|
||||||
|
.publish-info {
|
||||||
font-size: smaller;
|
font-size: smaller;
|
||||||
color: #555;
|
color: #555;
|
||||||
}
|
}
|
||||||
|
@ -68,7 +72,10 @@ const cleanContent = await cozify(article.content ?? '', Astro.url.origin)
|
||||||
}
|
}
|
||||||
|
|
||||||
content {
|
content {
|
||||||
p, table, ul, img {
|
p,
|
||||||
|
table,
|
||||||
|
ul,
|
||||||
|
img {
|
||||||
margin: 1em 0 !important;
|
margin: 1em 0 !important;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
@ -76,7 +83,8 @@ const cleanContent = await cozify(article.content ?? '', Astro.url.origin)
|
||||||
table {
|
table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
|
||||||
td, th {
|
td,
|
||||||
|
th {
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
}
|
}
|
||||||
|
@ -92,11 +100,13 @@ const cleanContent = await cozify(article.content ?? '', Astro.url.origin)
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
p, table, ul, img {
|
p,
|
||||||
|
table,
|
||||||
|
ul,
|
||||||
|
img {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
|
@ -1,12 +1,12 @@
|
||||||
---
|
---
|
||||||
import Icon from "astro-iconify";
|
import Icon from 'astro-iconify'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
url: string | null;
|
url: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const placeholder = "Type the article URL here";
|
const placeholder = 'Type the article URL here'
|
||||||
const { url } = Astro.props;
|
const { url } = Astro.props
|
||||||
---
|
---
|
||||||
|
|
||||||
<div id="address-bar">
|
<div id="address-bar">
|
||||||
|
@ -18,7 +18,7 @@ const { url } = Astro.props;
|
||||||
type="url"
|
type="url"
|
||||||
id="app-url"
|
id="app-url"
|
||||||
name="url"
|
name="url"
|
||||||
value={url ?? ""}
|
value={url ?? ''}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
@ -39,7 +39,6 @@ const { url } = Astro.props;
|
||||||
>
|
>
|
||||||
<Icon name="mdi:home" />
|
<Icon name="mdi:home" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -49,9 +48,7 @@ const { url } = Astro.props;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
form:has(
|
form:has(input[type='url']:focus) {
|
||||||
input[type="url"]:focus
|
|
||||||
) {
|
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
box-shadow: 0 1px 10px 0px 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));
|
box-shadow: 0 1px 3px 1px rgb(var(--gray-light));
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
input[type="url"]:focus {
|
input[type='url']:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="url"] {
|
input[type='url'] {
|
||||||
flex: 3;
|
flex: 3;
|
||||||
border: 0px;
|
border: 0px;
|
||||||
border-radius: 30px;
|
border-radius: 30px;
|
||||||
|
@ -131,7 +128,7 @@ const { url } = Astro.props;
|
||||||
color: blue !important;
|
color: blue !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn[disabled="true"] svg {
|
.btn[disabled='true'] svg {
|
||||||
color: rgb(var(--gray-light)) !important;
|
color: rgb(var(--gray-light)) !important;
|
||||||
cursor: default !important;
|
cursor: default !important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,25 @@
|
||||||
---
|
---
|
||||||
import '../../styles/reset.css';
|
import '../../styles/reset.css'
|
||||||
import '../../styles/variables.css';
|
import '../../styles/variables.css'
|
||||||
import '../../styles/blog.css';
|
import '../../styles/blog.css'
|
||||||
import { SITE_TITLE, SITE_AUTHOR, SITE_DESCRIPTION } from '../../consts';
|
import { SITE_TITLE, SITE_AUTHOR, SITE_DESCRIPTION } from '../../consts'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
title: string
|
||||||
description: string;
|
description: string
|
||||||
isArticle?: boolean;
|
isArticle?: boolean
|
||||||
image?: string;
|
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
|
description =
|
||||||
? SITE_DESCRIPTION
|
title === SITE_TITLE ? SITE_DESCRIPTION : `${description} • ${SITE_TITLE}`
|
||||||
: `${description} • ${SITE_TITLE}`
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!-- Global Metadata -->
|
<!-- Global Metadata -->
|
||||||
|
@ -26,8 +30,20 @@ description = title === SITE_TITLE
|
||||||
<meta name="generator" content={Astro.generator} />
|
<meta name="generator" content={Astro.generator} />
|
||||||
|
|
||||||
<!-- Font preloads -->
|
<!-- Font preloads -->
|
||||||
<link rel="preload" href="/fonts/atkinson-regular.woff" as="font" type="font/woff" crossorigin />
|
<link
|
||||||
<link rel="preload" href="/fonts/atkinson-bold.woff" as="font" type="font/woff" crossorigin />
|
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 -->
|
<!-- Primary Meta Tags -->
|
||||||
<title>{title} • {description}</title>
|
<title>{title} • {description}</title>
|
||||||
|
@ -36,9 +52,11 @@ description = title === SITE_TITLE
|
||||||
|
|
||||||
<!-- Open Graph / Facebook -->
|
<!-- Open Graph / Facebook -->
|
||||||
{
|
{
|
||||||
isArticle
|
isArticle ? (
|
||||||
? <meta property="og:type" content="article" />
|
<meta property="og:type" content="article" />
|
||||||
: <meta property="og:type" content="website" />
|
) : (
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
)
|
||||||
}
|
}
|
||||||
<meta property="og:url" content={Astro.url} />
|
<meta property="og:url" content={Astro.url} />
|
||||||
<meta property="og:title" content={title} />
|
<meta property="og:title" content={title} />
|
||||||
|
@ -46,5 +64,3 @@ description = title === SITE_TITLE
|
||||||
<meta property="og:image" content={new URL(image, Astro.url)} />
|
<meta property="og:image" content={new URL(image, Astro.url)} />
|
||||||
<meta property="og:site_name" content={SITE_TITLE} />
|
<meta property="og:site_name" content={SITE_TITLE} />
|
||||||
<meta property="article:author" content={SITE_AUTHOR} />
|
<meta property="article:author" content={SITE_AUTHOR} />
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,20 @@
|
||||||
---
|
---
|
||||||
import { SITE_AUTHOR, SITE_AUTHOR_EMAIL, SITE_AUTHOR_MASTODON, SITE_PROJECT_REPO } from "../../consts";
|
import {
|
||||||
const today = new Date();
|
SITE_AUTHOR,
|
||||||
|
SITE_AUTHOR_EMAIL,
|
||||||
|
SITE_AUTHOR_MASTODON,
|
||||||
|
SITE_PROJECT_REPO,
|
||||||
|
} from '../../consts'
|
||||||
|
const today = new Date()
|
||||||
---
|
---
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<p>© {today.getFullYear()} {SITE_AUTHOR}. All rights reserved.</p>
|
<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>
|
<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">
|
<div class="social-links">
|
||||||
<a href={SITE_AUTHOR_MASTODON} target="_blank">
|
<a href={SITE_AUTHOR_MASTODON} target="_blank">
|
||||||
<span class="sr-only">Follow Ayo on Mastodon</span>
|
<span class="sr-only">Follow Ayo on Mastodon</span>
|
||||||
|
@ -23,7 +32,12 @@ const today = new Date();
|
||||||
</a>
|
</a>
|
||||||
<a href={SITE_PROJECT_REPO} target="_blank">
|
<a href={SITE_PROJECT_REPO} target="_blank">
|
||||||
<span class="sr-only">Go to Cozy's GitHub repo</span>
|
<span class="sr-only">Go to 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
|
><path
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"
|
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
---
|
---
|
||||||
interface Props {
|
interface Props {
|
||||||
date: Date;
|
date: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
const { date } = Astro.props;
|
const { date } = Astro.props
|
||||||
---
|
---
|
||||||
|
|
||||||
<time datetime={date.toISOString()}>
|
<time datetime={date.toISOString()}>
|
||||||
|
|
|
@ -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>
|
<header>
|
||||||
|
@ -69,7 +74,6 @@ import { SITE_AUTHOR_MASTODON, SITE_DESCRIPTION, SITE_PROJECT_REPO, SITE_TITLE }
|
||||||
padding: 1px 0.5em 0;
|
padding: 1px 0.5em 0;
|
||||||
transition: 0.2s ease;
|
transition: 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nav .social-links a:hover {
|
nav .social-links a:hover {
|
||||||
|
|
|
@ -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
|
// eslint-disable-next-line no-useless-escape
|
||||||
const subpath = pathname.match(/[^\/]+/g);
|
const subpath = pathname.match(/[^\/]+/g)
|
||||||
const isActive = href === pathname || href === '/' + subpath?.[0];
|
const isActive = href === pathname || href === '/' + subpath?.[0]
|
||||||
---
|
---
|
||||||
|
|
||||||
<a href={href} class:list={[className, { active: isActive }]} {...props}>
|
<a href={href} class:list={[className, { active: isActive }]} {...props}>
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
export const SITE_TITLE = 'Cozy Blog';
|
export const SITE_TITLE = 'Cozy Blog'
|
||||||
export const SITE_AUTHOR = 'Ayo Ayco';
|
export const SITE_AUTHOR = 'Ayo Ayco'
|
||||||
export const SITE_AUTHOR_URL = 'https://ayo.ayco.io';
|
export const SITE_AUTHOR_URL = 'https://ayo.ayco.io'
|
||||||
export const SITE_AUTHOR_EMAIL = 'cozy@ayco.io';
|
export const SITE_AUTHOR_EMAIL = 'cozy@ayco.io'
|
||||||
export const SITE_AUTHOR_MASTODON = 'https://social.ayco.io/@ayo';
|
export const SITE_AUTHOR_MASTODON = 'https://social.ayco.io/@ayo'
|
||||||
export const SITE_PROJECT_REPO = 'https://github.com/ayoayco/Cozy';
|
export const SITE_PROJECT_REPO = 'https://github.com/ayoayco/Cozy'
|
||||||
export const SITE_DESCRIPTION = 'The Web is Yours.';
|
export const SITE_DESCRIPTION = 'The Web is Yours.'
|
||||||
|
|
||||||
export const VERSION = 'Yummy-Ydrasil';
|
export const VERSION = 'Yummy-Ydrasil'
|
||||||
|
|
|
@ -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.
|
Right now, it successfully extracts the content and delivers a clean page to your browser.
|
||||||
|
|
||||||
I'm working toward bringing the following in the coming weeks:
|
I'm working toward bringing the following in the coming weeks:
|
||||||
|
|
||||||
1. Save favorites to a library
|
1. Save favorites to a library
|
||||||
2. Offline access
|
2. Offline access
|
||||||
3. Smart Insights about the article
|
3. Smart Insights about the article
|
||||||
|
@ -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:
|
This is possible on all major browsers, including Safari on iOS (where I personally use this often). Some screenshots:
|
||||||
|
|
||||||
| Firefox | Chrome |
|
| Firefox | Chrome |
|
||||||
| --- | --- |
|
| ---------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|  |  |
|
|  |  |
|
||||||
|
|
||||||
## Join the Project!
|
## Join the Project!
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { defineCollection, z } from 'astro:content';
|
import { defineCollection, z } from 'astro:content'
|
||||||
|
|
||||||
const blog = defineCollection({
|
const blog = defineCollection({
|
||||||
type: 'content',
|
type: 'content',
|
||||||
|
@ -11,6 +11,6 @@ const blog = defineCollection({
|
||||||
updatedDate: z.coerce.date().optional(),
|
updatedDate: z.coerce.date().optional(),
|
||||||
heroImage: z.string().optional(),
|
heroImage: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
});
|
})
|
||||||
|
|
||||||
export const collections = { blog };
|
export const collections = { blog }
|
||||||
|
|
|
@ -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
|
You visit a news website, for example, and you just know the content are mostly just a bait
|
||||||
|
|
||||||
|
|
||||||
Browsers are not helping. AI
|
Browsers are not helping. AI
|
||||||
|
|
||||||
Having a web page let's me skip all the noise that plague almost all modern websites
|
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)
|
[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.
|
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,14 +1,14 @@
|
||||||
---
|
---
|
||||||
import BaseHead from "../components/blog/BaseHead.astro";
|
import BaseHead from '../components/blog/BaseHead.astro'
|
||||||
import Header from "../components/blog/Header.astro";
|
import Header from '../components/blog/Header.astro'
|
||||||
import Footer from "../components/blog/Footer.astro";
|
import Footer from '../components/blog/Footer.astro'
|
||||||
import FormattedDate from "../components/blog/FormattedDate.astro";
|
import FormattedDate from '../components/blog/FormattedDate.astro'
|
||||||
import type { CollectionEntry } from "astro:content";
|
import type { CollectionEntry } from 'astro:content'
|
||||||
import { SITE_AUTHOR, SITE_AUTHOR_URL, SITE_AUTHOR_EMAIL } from "../consts";
|
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">
|
<html lang="en">
|
||||||
|
@ -64,7 +64,6 @@ const { title, description, pubDate, updatedDate, heroImage } = Astro.props;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
box-shadow: 0 2px 8px var(--accent);
|
box-shadow: 0 2px 8px var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,16 +137,7 @@ const { title, description, pubDate, updatedDate, heroImage } = Astro.props;
|
||||||
<main>
|
<main>
|
||||||
<article>
|
<article>
|
||||||
<div class="hero-image">
|
<div class="hero-image">
|
||||||
{
|
{heroImage && <img width={700} height={510} src={heroImage} alt="" />}
|
||||||
heroImage && (
|
|
||||||
<img
|
|
||||||
width={700}
|
|
||||||
height={510}
|
|
||||||
src={heroImage}
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="prose">
|
<div class="prose">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
|
@ -163,7 +153,12 @@ const { title, description, pubDate, updatedDate, heroImage } = Astro.props;
|
||||||
</div>
|
</div>
|
||||||
<h1>{title}</h1>
|
<h1>{title}</h1>
|
||||||
<address style="display:inline">
|
<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>
|
</address>
|
||||||
</div>
|
</div>
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -1,19 +1,16 @@
|
||||||
---
|
---
|
||||||
import App from "../layouts/App.astro";
|
import App from '../layouts/App.astro'
|
||||||
import Library from "../components/Library.astro";
|
import Library from '../components/Library.astro'
|
||||||
import Footer from "../components/Footer.astro";
|
import Footer from '../components/Footer.astro'
|
||||||
import SimpleAddressBar from "../components/SimpleAddressBar.astro";
|
import SimpleAddressBar from '../components/SimpleAddressBar.astro'
|
||||||
|
|
||||||
export const prerender = false;
|
|
||||||
|
|
||||||
|
export const prerender = false
|
||||||
---
|
---
|
||||||
|
|
||||||
<App article={null}>
|
<App article={null}>
|
||||||
<SimpleAddressBar url='' />
|
<SimpleAddressBar url="" />
|
||||||
<div slot="post" id="router-outlet">
|
<div slot="post" id="router-outlet">
|
||||||
<h1>
|
<h1>404: Not Found</h1>
|
||||||
404: Not Found
|
|
||||||
</h1>
|
|
||||||
</div>
|
</div>
|
||||||
<Library slot="library" skipSave />
|
<Library slot="library" skipSave />
|
||||||
<Footer slot="footer" />
|
<Footer slot="footer" />
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
---
|
---
|
||||||
import { type CollectionEntry, getCollection } from 'astro:content';
|
import { type CollectionEntry, getCollection } from 'astro:content'
|
||||||
import Blog from '../../layouts/Blog.astro';
|
import Blog from '../../layouts/Blog.astro'
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const posts = await getCollection('blog');
|
const posts = await getCollection('blog')
|
||||||
return posts.map((post) => ({
|
return posts.map((post) => ({
|
||||||
params: { slug: post.slug },
|
params: { slug: post.slug },
|
||||||
props: post,
|
props: post,
|
||||||
}));
|
}))
|
||||||
}
|
}
|
||||||
type Props = CollectionEntry<'blog'>;
|
type Props = CollectionEntry<'blog'>
|
||||||
|
|
||||||
const post = Astro.props;
|
const post = Astro.props
|
||||||
const { Content } = await post.render();
|
const { Content } = await post.render()
|
||||||
---
|
---
|
||||||
|
|
||||||
<Blog {...post.data}>
|
<Blog {...post.data}>
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
---
|
---
|
||||||
import BaseHead from '../../components/blog/BaseHead.astro';
|
import BaseHead from '../../components/blog/BaseHead.astro'
|
||||||
import Header from '../../components/blog/Header.astro';
|
import Header from '../../components/blog/Header.astro'
|
||||||
import Footer from '../../components/blog/Footer.astro';
|
import Footer from '../../components/blog/Footer.astro'
|
||||||
import FormattedDate from '../../components/blog/FormattedDate.astro';
|
import FormattedDate from '../../components/blog/FormattedDate.astro'
|
||||||
import { getCollection } from 'astro:content';
|
import { getCollection } from 'astro:content'
|
||||||
import {SITE_TITLE, SITE_DESCRIPTION} from '../../consts';
|
import { SITE_TITLE, SITE_DESCRIPTION } from '../../consts'
|
||||||
|
|
||||||
const posts = (await getCollection('blog')).sort(
|
const posts = (await getCollection('blog')).sort(
|
||||||
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
|
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
|
||||||
);
|
)
|
||||||
---
|
---
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
|
@ -115,13 +115,9 @@ const posts = (await getCollection('blog')).sort(
|
||||||
<FormattedDate date={post.data.pubDate} />
|
<FormattedDate date={post.data.pubDate} />
|
||||||
</small>
|
</small>
|
||||||
<h4 class="title">
|
<h4 class="title">
|
||||||
<a href={`/blog/${post.slug}/`}>
|
<a href={`/blog/${post.slug}/`}>{post.data.title}</a>
|
||||||
{post.data.title}
|
|
||||||
</a>
|
|
||||||
</h4>
|
</h4>
|
||||||
<p class="description">
|
<p class="description">{post.data.description}</p>
|
||||||
{post.data.description}
|
|
||||||
</p>
|
|
||||||
</li>
|
</li>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,28 @@
|
||||||
---
|
---
|
||||||
import { ArticleData, extract } from "@extractus/article-extractor";
|
import { ArticleData, extract } from '@extractus/article-extractor'
|
||||||
import SimpleAddressBar from "../components/SimpleAddressBar.astro";
|
import SimpleAddressBar from '../components/SimpleAddressBar.astro'
|
||||||
import Post from "../components/Post.astro";
|
import Post from '../components/Post.astro'
|
||||||
import App from "../layouts/App.astro";
|
import App from '../layouts/App.astro'
|
||||||
import Library from "../components/Library.astro";
|
import Library from '../components/Library.astro'
|
||||||
import Footer from "../components/Footer.astro";
|
import Footer from '../components/Footer.astro'
|
||||||
|
|
||||||
export const prerender = false;
|
export const prerender = false
|
||||||
|
|
||||||
let url = Astro.url.searchParams.get('url');
|
let url = Astro.url.searchParams.get('url')
|
||||||
let article: ArticleData | null = {url: '/'};
|
let article: ArticleData | null = { url: '/' }
|
||||||
|
|
||||||
while (url?.startsWith(Astro.url.origin)) {
|
while (url?.startsWith(Astro.url.origin)) {
|
||||||
url = new URL(url).searchParams.get('url');
|
url = new URL(url).searchParams.get('url')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url)
|
if (url)
|
||||||
try {
|
try {
|
||||||
article = await extract(url);
|
article = await extract(url)
|
||||||
} catch {
|
} catch {
|
||||||
article = null;
|
article = null
|
||||||
}
|
}
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<App article={article}>
|
<App article={article}>
|
||||||
<SimpleAddressBar url={url} />
|
<SimpleAddressBar url={url} />
|
||||||
<div slot="post" id="router-outlet">
|
<div slot="post" id="router-outlet">
|
||||||
|
|
|
@ -32,7 +32,6 @@ body {
|
||||||
color: rgb(var(--gray-dark));
|
color: rgb(var(--gray-dark));
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
:root {
|
:root {
|
||||||
--accent: #3054bf;
|
--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;
|
--accent-dark: #203880;
|
||||||
--black: 15, 18, 25;
|
--black: 15, 18, 25;
|
||||||
--gray: 96, 115, 159;
|
--gray: 96, 115, 159;
|
||||||
--gray-light: 229, 233, 240;
|
--gray-light: 229, 233, 240;
|
||||||
--gray-dark: 34, 41, 57;
|
--gray-dark: 34, 41, 57;
|
||||||
--gray-gradient: rgba(var(--gray-light), 50%), #fff;
|
--gray-gradient: rgba(var(--gray-light), 50%), #fff;
|
||||||
--box-shadow: 0 2px 6px rgba(var(--gray), 25%), 0 8px 24px rgba(var(--gray), 33%),
|
--box-shadow: 0 2px 6px rgba(var(--gray), 25%),
|
||||||
0 16px 32px rgba(var(--gray), 33%);
|
0 8px 24px rgba(var(--gray), 33%), 0 16px 32px rgba(var(--gray), 33%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
93
src/sw.mjs
93
src/sw.mjs
|
@ -6,86 +6,88 @@
|
||||||
const cacheName = `${__prefix ?? 'app'}-v${__version ?? '000'}`
|
const cacheName = `${__prefix ?? 'app'}-v${__version ?? '000'}`
|
||||||
|
|
||||||
const cleanOldCaches = async () => {
|
const cleanOldCaches = async () => {
|
||||||
const allowCacheNames = ['cozy-reader', cacheName];
|
const allowCacheNames = ['cozy-reader', cacheName]
|
||||||
const allCaches = await caches.keys();
|
const allCaches = await caches.keys()
|
||||||
allCaches.forEach(key => {
|
allCaches.forEach((key) => {
|
||||||
if (!allowCacheNames.includes(key)) {
|
if (!allowCacheNames.includes(key)) {
|
||||||
console.info('Deleting old cache', key);
|
console.info('Deleting old cache', key)
|
||||||
caches.delete(key);
|
caches.delete(key)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const addResourcesToCache = async (resources) => {
|
const addResourcesToCache = async (resources) => {
|
||||||
const cache = await caches.open(cacheName);
|
const cache = await caches.open(cacheName)
|
||||||
console.info('adding resources to cache...', resources)
|
console.info('adding resources to cache...', resources)
|
||||||
try {
|
try {
|
||||||
await cache.addAll(resources);
|
await cache.addAll(resources)
|
||||||
} catch (error) {
|
} 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,
|
resources,
|
||||||
error
|
error,
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const putInCache = async (request, response) => {
|
const putInCache = async (request, response) => {
|
||||||
const cache = await caches.open(cacheName);
|
const cache = await caches.open(cacheName)
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
console.info('adding one response to cache...', request.url)
|
console.info('adding one response to cache...', request.url)
|
||||||
|
|
||||||
// if exists, replace
|
// if exists, replace
|
||||||
cache.keys().then( keys => {
|
cache.keys().then((keys) => {
|
||||||
if (keys.includes(request)) {
|
if (keys.includes(request)) {
|
||||||
cache.delete(request);
|
cache.delete(request)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
cache.put(request, response);
|
cache.put(request, response)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const cacheAndRevalidate = async ({ request, fallbackUrl }) => {
|
const cacheAndRevalidate = async ({ request, fallbackUrl }) => {
|
||||||
|
const cache = await caches.open(cacheName)
|
||||||
const cache = await caches.open(cacheName);
|
|
||||||
|
|
||||||
// Try get the resource from the cache
|
// Try get the resource from the cache
|
||||||
const responseFromCache = await cache.match(request);
|
const responseFromCache = await cache.match(request)
|
||||||
if (responseFromCache) {
|
if (responseFromCache) {
|
||||||
console.info('using cached response...', responseFromCache.url);
|
console.info('using cached response...', responseFromCache.url)
|
||||||
// get network response for revalidation of cached assets
|
// get network response for revalidation of cached assets
|
||||||
fetch(request.clone()).then((responseFromNetwork) => {
|
fetch(request.clone())
|
||||||
|
.then((responseFromNetwork) => {
|
||||||
if (responseFromNetwork) {
|
if (responseFromNetwork) {
|
||||||
console.info('fetched updated resource...', responseFromNetwork.url)
|
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)
|
console.info('failed to fetch updated resource', error)
|
||||||
});
|
})
|
||||||
|
|
||||||
return responseFromCache;
|
return responseFromCache
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Try to get the resource from the network for 5 seconds
|
// 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
|
// response may be used only once
|
||||||
// we need to save clone to put one copy in cache
|
// we need to save clone to put one copy in cache
|
||||||
// and serve second one
|
// and serve second one
|
||||||
putInCache(request, responseFromNetwork.clone());
|
putInCache(request, responseFromNetwork.clone())
|
||||||
console.info('using network response', responseFromNetwork.url)
|
console.info('using network response', responseFromNetwork.url)
|
||||||
return responseFromNetwork;
|
return responseFromNetwork
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
||||||
// Try the fallback
|
// Try the fallback
|
||||||
const fallbackResponse = await cache.match(fallbackUrl);
|
const fallbackResponse = await cache.match(fallbackUrl)
|
||||||
if (fallbackResponse) {
|
if (fallbackResponse) {
|
||||||
console.info('using fallback cached response...', fallbackResponse.url)
|
console.info('using fallback cached response...', fallbackResponse.url)
|
||||||
return fallbackResponse;
|
return fallbackResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
// when even the fallback response is not available,
|
// when even the fallback response is not available,
|
||||||
|
@ -94,33 +96,30 @@ const cacheAndRevalidate = async ({ request, fallbackUrl }) => {
|
||||||
return new Response('Network error happened', {
|
return new Response('Network error happened', {
|
||||||
status: 408,
|
status: 408,
|
||||||
headers: { 'Content-Type': 'text/plain' },
|
headers: { 'Content-Type': 'text/plain' },
|
||||||
});
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
self.addEventListener('activate', (event) => {
|
self.addEventListener('activate', (event) => {
|
||||||
console.info('activating service worker...')
|
console.info('activating service worker...')
|
||||||
cleanOldCaches();
|
cleanOldCaches()
|
||||||
});
|
})
|
||||||
|
|
||||||
self.addEventListener('install', (event) => {
|
self.addEventListener('install', (event) => {
|
||||||
|
|
||||||
console.info('installing service worker...')
|
console.info('installing service worker...')
|
||||||
self.skipWaiting(); // go straight to activate
|
self.skipWaiting() // go straight to activate
|
||||||
|
|
||||||
event.waitUntil(
|
event.waitUntil(addResourcesToCache(__assets ?? []))
|
||||||
addResourcesToCache(__assets ?? [])
|
})
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
self.addEventListener('fetch', (event) => {
|
self.addEventListener('fetch', (event) => {
|
||||||
console.info('fetch happened', {data: event});
|
console.info('fetch happened', { data: event })
|
||||||
|
|
||||||
event.respondWith(
|
event.respondWith(
|
||||||
cacheAndRevalidate({
|
cacheAndRevalidate({
|
||||||
request: event.request,
|
request: event.request,
|
||||||
fallbackUrl: './',
|
fallbackUrl: './',
|
||||||
})
|
})
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export function getPostCard(html: HTMLHtmlElement) {
|
export function getPostCard(html: HTMLHtmlElement) {
|
||||||
const {title, description, image, source, published} = getPostMeta(html);
|
const { title, description, image, source, published } = getPostMeta(html)
|
||||||
const postCard = `
|
const postCard = `
|
||||||
<div class="post-card">
|
<div class="post-card">
|
||||||
<div class="post-card__image">
|
<div class="post-card__image">
|
||||||
|
@ -17,99 +17,109 @@ 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: string | null, url, postDivSelector: string, preventPushState = false) {
|
export function renderPost(
|
||||||
const postDiv = document.querySelector<HTMLDivElement>(`#${postDivSelector}`);
|
responseText: string | null,
|
||||||
let postText = '';
|
url,
|
||||||
let cozyUrl = '/';
|
postDivSelector: string,
|
||||||
let cozyTitle = 'Cozy';
|
preventPushState = false
|
||||||
|
) {
|
||||||
|
const postDiv = document.querySelector<HTMLDivElement>(`#${postDivSelector}`)
|
||||||
|
let postText = ''
|
||||||
|
let cozyUrl = '/'
|
||||||
|
let cozyTitle = 'Cozy'
|
||||||
if (responseText) {
|
if (responseText) {
|
||||||
const html = document.createElement('html');
|
const html = document.createElement('html')
|
||||||
html.innerHTML = responseText;
|
html.innerHTML = responseText
|
||||||
const newPost = html.querySelector('body')?.querySelector('#post');
|
const newPost = html.querySelector('body')?.querySelector('#post')
|
||||||
postText = newPost?.outerHTML || '';
|
postText = newPost?.outerHTML || ''
|
||||||
cozyUrl = html.querySelector('meta[property="cozy:url"]')?.getAttribute('content') ?? '/';
|
cozyUrl =
|
||||||
cozyTitle = `${getCozyTitle(html)} | Cozy`;
|
html
|
||||||
|
.querySelector('meta[property="cozy:url"]')
|
||||||
|
?.getAttribute('content') ?? '/'
|
||||||
|
cozyTitle = `${getCozyTitle(html)} | Cozy`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (postDiv) {
|
if (postDiv) {
|
||||||
postDiv.innerHTML = postText;
|
postDiv.innerHTML = postText
|
||||||
|
|
||||||
const appUrl = document.getElementById('app-url') as HTMLInputElement;
|
const appUrl = document.getElementById('app-url') as HTMLInputElement
|
||||||
const backBtn = document.querySelector<HTMLButtonElement>('#app-back');
|
const backBtn = document.querySelector<HTMLButtonElement>('#app-back')
|
||||||
const submitBtn = document.querySelector<HTMLButtonElement>('#submit');
|
const submitBtn = document.querySelector<HTMLButtonElement>('#submit')
|
||||||
if (cozyUrl !== '/') {
|
if (cozyUrl !== '/') {
|
||||||
appUrl.value = cozyUrl || '';
|
appUrl.value = cozyUrl || ''
|
||||||
backBtn?.removeAttribute('disabled');
|
backBtn?.removeAttribute('disabled')
|
||||||
submitBtn?.removeAttribute('disabled');
|
submitBtn?.removeAttribute('disabled')
|
||||||
document.title = cozyTitle;
|
document.title = cozyTitle
|
||||||
} else {
|
} else {
|
||||||
appUrl.value = '';
|
appUrl.value = ''
|
||||||
backBtn?.setAttribute('disabled', 'true');
|
backBtn?.setAttribute('disabled', 'true')
|
||||||
submitBtn?.setAttribute('disabled', 'true');
|
submitBtn?.setAttribute('disabled', 'true')
|
||||||
document.title = `Cozy`;
|
document.title = `Cozy`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!preventPushState) {
|
if (!preventPushState) {
|
||||||
window.history.pushState({url}, '', url);
|
window.history.pushState({ url }, '', url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPostMeta(html: HTMLHtmlElement) {
|
function getPostMeta(html: HTMLHtmlElement) {
|
||||||
const title = getCozyTitle(html);
|
const title = getCozyTitle(html)
|
||||||
const description = html
|
const description = html
|
||||||
.querySelector('meta[property="cozy:description"]')
|
.querySelector('meta[property="cozy:description"]')
|
||||||
?.getAttribute("content");
|
?.getAttribute('content')
|
||||||
const image = html
|
const image = html
|
||||||
.querySelector('meta[property="cozy:image"]')
|
.querySelector('meta[property="cozy:image"]')
|
||||||
?.getAttribute("content");
|
?.getAttribute('content')
|
||||||
const source = html
|
const source = html
|
||||||
.querySelector('meta[property="cozy:source"]')
|
.querySelector('meta[property="cozy:source"]')
|
||||||
?.getAttribute("content");
|
?.getAttribute('content')
|
||||||
const published = html
|
const published = html
|
||||||
.querySelector('meta[property="cozy:published"]')
|
.querySelector('meta[property="cozy:published"]')
|
||||||
?.getAttribute("content");
|
?.getAttribute('content')
|
||||||
|
|
||||||
return {title, description, image, source, published};
|
return { title, description, image, source, published }
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCozyTitle(html: HTMLHtmlElement): string | undefined {
|
function getCozyTitle(html: HTMLHtmlElement): string | undefined {
|
||||||
return 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
|
* backwards compatibility for stuff before we implemented cozy:meta tags
|
||||||
* REMOVE ON V1 release
|
* REMOVE ON V1 release
|
||||||
*/
|
*/
|
||||||
?? html.querySelector("title")?.innerHTML
|
html.querySelector('title')?.innerHTML?.replace('Cozy 🧸 | ', '')
|
||||||
?.replace("Cozy 🧸 | ", "")
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { parse, render, transform, walkSync } from 'ultrahtml'
|
||||||
import sanitize from 'ultrahtml/transformers/sanitize'
|
import sanitize from 'ultrahtml/transformers/sanitize'
|
||||||
|
|
||||||
export async function cozify(html: string, baseUrl: string): Promise<string> {
|
export async function cozify(html: string, baseUrl: string): Promise<string> {
|
||||||
|
|
||||||
// remove target="_blank" from links
|
// remove target="_blank" from links
|
||||||
const ast = parse(html)
|
const ast = parse(html)
|
||||||
walkSync(ast, (node) => {
|
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, [
|
return transform(newHtml, [
|
||||||
sanitize({
|
sanitize({
|
||||||
dropElements: ['script'],
|
dropElements: ['script'],
|
||||||
dropAttributes: {
|
dropAttributes: {
|
||||||
target: ['a']
|
target: ['a'],
|
||||||
}
|
},
|
||||||
})
|
}),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,31 +1,31 @@
|
||||||
import { describe, expect, test } from "vitest";
|
import { describe, expect, test } from 'vitest'
|
||||||
import { cozify } from "../src/utils/sanitizer";
|
import { cozify } from '../src/utils/sanitizer'
|
||||||
|
|
||||||
describe("cozify()", async () => {
|
describe('cozify()', async () => {
|
||||||
const baseUrl = "https://cozy.pub";
|
const baseUrl = 'https://cozy.pub'
|
||||||
|
|
||||||
test("should remove scripts", async () => {
|
test('should remove scripts', async () => {
|
||||||
const html = "<h1>HELLO</h1><script>console.log()</script>";
|
const html = '<h1>HELLO</h1><script>console.log()</script>'
|
||||||
const result = await cozify(html, baseUrl);
|
const result = await cozify(html, baseUrl)
|
||||||
expect(result).not.toContain("<script>");
|
expect(result).not.toContain('<script>')
|
||||||
});
|
})
|
||||||
|
|
||||||
test("should remove target=_blank from links", async () => {
|
test('should remove target=_blank from links', async () => {
|
||||||
const html = "<a href=# target='_blank'>hey</a>";
|
const html = "<a href=# target='_blank'>hey</a>"
|
||||||
const result = await cozify(html, baseUrl);
|
const result = await cozify(html, baseUrl)
|
||||||
expect(result).not.toContain("target");
|
expect(result).not.toContain('target')
|
||||||
console.log(result);
|
console.log(result)
|
||||||
});
|
})
|
||||||
|
|
||||||
test("should add base url to href of links", async () => {
|
test('should add base url to href of links', async () => {
|
||||||
const html = "<a href=#>hey</a>";
|
const html = '<a href=#>hey</a>'
|
||||||
const result = await cozify(html, baseUrl);
|
const result = await cozify(html, baseUrl)
|
||||||
expect(result).toContain('href="https://cozy.pub?url=#"');
|
expect(result).toContain('href="https://cozy.pub?url=#"')
|
||||||
});
|
})
|
||||||
|
|
||||||
test("should add prefetch=true to links", async () => {
|
test('should add prefetch=true to links', async () => {
|
||||||
const html = "<a href=#>hey</a>";
|
const html = '<a href=#>hey</a>'
|
||||||
const result = await cozify(html, baseUrl);
|
const result = await cozify(html, baseUrl)
|
||||||
expect(result).toContain('prefetch="true"');
|
expect(result).toContain('prefetch="true"')
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
|
Loading…
Reference in a new issue