Compare commits

...

16 commits

21 changed files with 1861 additions and 741 deletions

1
.gitignore vendored
View file

@ -1,5 +1,6 @@
# build output
dist/
dc09ru.zip
# generated types
.astro/

View file

@ -1,14 +1,15 @@
import { defineConfig } from 'astro/config';
import compress from "astro-compress";
import remarkPostMeta from './remark-post-meta.mjs';
import remarkUnwrapImages from 'remark-unwrap-images';
import { rehypeHeadingIds } from '@astrojs/markdown-remark';
import rehypeSlug from 'rehype-slug';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import rehypeFigure from 'rehype-figure';
// https://astro.build/config
export default defineConfig({
integrations: [compress()],
build: {
@ -18,7 +19,6 @@ export default defineConfig({
syntaxHighlight: "shiki",
remarkPlugins: [
remarkPostMeta,
remarkUnwrapImages,
],
rehypePlugins: [
rehypeSlug,
@ -27,10 +27,7 @@ export default defineConfig({
rehypeAutolinkHeadings,
{ behavior: "append" },
],
[
rehypeFigure,
{ className: "rehype-figure" },
],
rehypeFigure,
],
},
});

2172
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -13,11 +13,12 @@
"astro": "^2.3.0",
"astro-compress": "^1.1.42",
"less": "^4.1.3",
"mdast-util-to-string": "^3.2.0",
"reading-time": "^1.5.0",
"rehype-autolink-headings": "^6.1.1",
"rehype-figure": "^1.0.1",
"rehype-slug": "^5.1.0",
"remark-unwrap-images": "^3.0.1"
"remark-unwrap-images": "^3.0.1",
"unist-util-select": "^4.0.3",
"unist-util-visit": "^4.1.2"
}
}

View file

Before

Width:  |  Height:  |  Size: 229 KiB

After

Width:  |  Height:  |  Size: 229 KiB

View file

@ -1,16 +1,26 @@
import getReadingTime from 'reading-time';
import { toString as mdToString } from 'mdast-util-to-string';
import { select } from 'unist-util-select'
import { visit } from 'unist-util-visit'
import getReadingTime from 'reading-time'
const ELLIPSIS = '\u2026';
export default function remarkPostMeta() {
return (tree, { data }) => {
const firstPara = select(':not(heading) paragraph:first-of-type', tree)
export default function() {
return function (tree, { data }) {
const textOnPage = mdToString(tree);
const firstParaText = mdToString(firstPara)
const articleText = mdToString(tree)
const readingTime = getReadingTime(textOnPage);
data.astro.frontmatter.readingTime = readingTime.text;
const readingTime = getReadingTime(articleText)
data.astro.frontmatter.readingTime = readingTime.minutes
const description = textOnPage.slice(0, 100) + ELLIPSIS;
data.astro.frontmatter.description = description;
};
data.astro.frontmatter.description = firstParaText
}
}
function mdToString(tree) {
let str = ''
visit(tree, 'text', node => {
str += node.value
str += ' '
})
return str.trimEnd()
}

View file

@ -0,0 +1,44 @@
---
import { AstroComponentFactory } from 'astro/dist/runtime/server';
export interface Props {
PostContent: AstroComponentFactory;
title: string;
description: string;
readingTime: number;
date: Date;
image: string;
preview?: boolean;
}
const {
PostContent, title, description,
readingTime, date, image,
preview = false,
} = Astro.props;
const locale = 'ru';
---
<header>
<h1>{title}</h1>
<address>
on
<time datetime={date?.toISOString()}>{date?.toLocaleDateString(locale)}</time>
by
<a href="https://t.me/dcat09" rel="author">DarkCat09</a>
//
<span id="reading-time">
{readingTime.toFixed(1)} min read
</span>
</address>
</header>
{
!preview ?
<article>
<PostContent />
</article> :
<div>
{description}
</div>
}

View file

@ -0,0 +1,113 @@
---
import Article from './Article.astro';
import MenuItem from './MenuItem.astro';
import postRenderer from '../post-renderer.mjs';
import { postType } from '../post-renderer.mjs';
export interface Props {
post: postType;
}
const { post } = Astro.props;
const {
PostContent, title, description,
readingTime, date, image,
} = await postRenderer(post);
const link = `/blog/${post.slug}`;
---
<div class="card card-elevated" id={post.slug}>
<Article
PostContent={PostContent}
title={title}
description={description}
readingTime={readingTime}
date={date}
image={image}
preview={true}
/>
<ul>
<MenuItem
name={/*Comments*/""}
link={`/blog/comments#${post.slug}`}
icon="&#xf075;"
atLeft={false}
/>
<MenuItem
name={/*Copy Link*/""}
link="javascript:void(0)"
icon="&#xf0c5;"
atLeft={false}>
<span slot="js-dataset"
data-btn="copylink"
data-url={link}>
</span>
</MenuItem>
<MenuItem
name={/*Share*/""}
link="javascript:void(0)"
icon="&#xf064;"
atLeft={false}>
<span slot="js-dataset"
data-btn="share"
data-title={title}
data-url={link}>
</span>
</MenuItem>
<MenuItem
name="Read"
link={link}
icon="&#xf105;"
atLeft={false}
/>
</ul>
</div>
<style>
.card {
width: 100%;
}
ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: row;
justify-content: end;
}
</style>
<script>
const onclickCopy = (ev: Event) => {
const li = ev.currentTarget as HTMLLIElement
const js = li.querySelector('[data-btn]') as HTMLElement
navigator.clipboard.writeText(
location.origin + js.dataset.url
)
}
const onclickShare = (ev: Event) => {
const li = ev.currentTarget as HTMLLIElement
const js = li.querySelector('[data-btn]') as HTMLElement
const data = js.dataset
navigator.share({
title: "Share the article",
text: data.title,
url: data.url,
})
}
const btnsCopy = document.querySelectorAll('[data-btn="copylink"]')
const btnsShare = document.querySelectorAll('[data-btn="share"]')
for (let el of btnsCopy) {
(el.parentElement as HTMLLIElement).addEventListener('click', onclickCopy)
}
for (let el of btnsShare) {
(el.parentElement as HTMLLIElement).addEventListener('click', onclickShare)
}
</script>

View file

@ -5,44 +5,42 @@ export interface Props {
name: string;
link: string;
icon: string;
atLeft?: boolean;
}
const { name, link, icon } = Astro.props;
const { name, link, icon, atLeft = true } = Astro.props;
---
<li>
<slot name="js-dataset"></slot>
<a href={link}>
<span class="icon-wrapper">
{!atLeft ? name : ""}
<span class:list={["icon-wrapper", {"highlight-icon": name == ""}]}>
<Icon code={icon} />
</span>
{name}
{atLeft ? name : ""}
</a>
</li>
<style lang="less">
li {
// 0.6rem = horizontal padding
padding: 0 0.6rem;
font-size: 1.15rem;
&:first-child {
padding-left: 0;
}
&:last-child {
padding-right: 0;
}
}
a {
display: flex;
flex-direction: row;
align-items: center;
font-size: 1.15rem;
}
.icon-wrapper {
margin-right: 0.25rem;
margin-right: 0.2rem;
width: 1.5rem;
font-size: 1.5rem;
color: var(--fg-sec);
&.highlight-icon:hover {
color: var(--fg);
}
}
</style>

View file

@ -23,5 +23,4 @@ console.log(Object.keys({a:'a',b:'b'}))
> Quotation
![Forest](https://dc09.ru/forest.jpg)
123
![Forest](/assets/forest.jpg)

View file

@ -14,9 +14,10 @@ const { title, description } = Astro.props;
<Layout title={"Blog | " + title} description={description}>
<slot name="metadata" slot="metadata" />
<Menu>
<MenuItem name="All articles" icon="&#xf015;" link="/blog" />
<MenuItem name="Home" icon="&#xf104;" link="/" />
<MenuItem name="Articles" icon="&#xf015;" link="/blog" />
<MenuItem name="Shorts" icon="&#xf0e7;" link="/blog/shorts" />
<MenuItem name="Random post" icon="&#xf522;" link="/blog/random" />
<MenuItem name="Random" icon="&#xf522;" link="/blog/random" />
<MenuItem name="Search" icon="&#xf002;" link="/blog/search" />
</Menu>
<main>

View file

@ -33,6 +33,9 @@ const { title, description } = Astro.props;
margin: 0;
padding: 0;
box-sizing: border-box;
padding: 0 0.5rem;
background: var(--bg);
color: var(--fg);
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Liberation Sans', 'Helvetica Neue', sans-serif;
@ -58,9 +61,16 @@ const { title, description } = Astro.props;
}
.card {
border: 0.125rem dashed var(--bg-sec);
border-radius: var(--bdrs);
padding: 0.625rem;
&.card-dashed {
border: 0.125rem dashed var(--bg-sec);
}
&.card-elevated {
background: var(--accent-bg);
}
}
hgroup > * {
@ -84,6 +94,10 @@ const { title, description } = Astro.props;
font-size: 1.1rem;
}
h1, h2, h3, h4, h5, h6 {
margin: 0;
}
a, .link {
color: var(--accent-fg);
text-decoration: none;

View file

@ -8,6 +8,7 @@ import ThemeModal from "../components/ThemeModal.astro";
<ul>
<slot />
</ul>
<ul class="menu-sep"></ul>
<ul>
<MenuItem name="Theme" icon="&#xf53f;" link="#theme" />
</ul>
@ -16,6 +17,9 @@ import ThemeModal from "../components/ThemeModal.astro";
<style lang="less">
@import "/src/styles/max_width.less";
// distance between menu items
@items-dist: 0.8rem;
nav {
margin: 0.4rem auto;
max-width: @max-width;
@ -33,5 +37,10 @@ import ThemeModal from "../components/ThemeModal.astro";
display: flex;
flex-direction: row;
flex-wrap: wrap;
column-gap: @items-dist;
}
ul.menu-sep {
min-width: @items-dist;
}
</style>

View file

@ -25,6 +25,7 @@ const { id } = Astro.props;
display: none;
position: absolute;
left: 0;
z-index: 100;
width: 100vw;
height: 100vh;
@ -37,7 +38,7 @@ const { id } = Astro.props;
}
}
.modal-bg-close {
a.modal-bg-close {
display: block;
position: absolute;

View file

@ -1,24 +1,9 @@
---
import BlogLayout from "../../layouts/BlogLayout.astro";
import Article from "../../components/Article.astro";
import { getCollection } from "astro:content";
import { AstroComponentFactory } from "astro/dist/runtime/server";
import { MarkdownHeading } from "astro";
type postType = {
id: string;
slug: string;
body: string;
collection: "blog";
data: any;
} & {
render(): Promise<{
Content: AstroComponentFactory;
headings: MarkdownHeading[];
remarkPluginFrontmatter: Record<string, any>;
}>;
};
import postRenderer from "../../post-renderer.mjs";
export async function getStaticPaths() {
const posts = await getCollection("blog");
@ -31,29 +16,14 @@ export async function getStaticPaths() {
}
const { slug } = Astro.params;
const post: postType = Astro.props.post;
const { post } = Astro.props;
const postObj = await post.render();
const fm = postObj.remarkPluginFrontmatter;
const {
PostContent, title, description,
readingTime, date, image,
} = await postRenderer(post);
const title = postObj.headings[0]?.text || '';
const description = fm.description;
const readingTime = fm.readingTime;
let dateStr = slug.split("-", 1)[0];
let date: Date | undefined;
if (isNaN(Number(dateStr))) {
dateStr = undefined;
}
else {
date = new Date(
`${dateStr.slice(0,4)}-` +
`${dateStr.slice(4,6)}-` +
`${dateStr.slice(6,8)}`
)
}
const image = (post.body.match(/(?:\s|^)!\[.*\]\((.*?)\)/) || [])[1];
const locale = 'ru-RU';
---
<BlogLayout title={title} description={description}>
@ -69,28 +39,18 @@ const image = (post.body.match(/(?:\s|^)!\[.*\]\((.*?)\)/) || [])[1];
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={image} />
</Fragment>
<header>
<h1>{title}</h1>
<address>
on
<time datetime={date?.toISOString()}>{date?.toLocaleDateString()}</time>
by
<a href="https://t.me/dcat09" rel="author">DarkCat09</a>
//
<span id="reading-time">
{readingTime}
</span>
</address>
</header>
<article>
<postObj.Content />
</article>
<Article
PostContent={PostContent}
title={title}
description={description}
readingTime={readingTime}
date={date}
image={image}
/>
</BlogLayout>
<style lang="less" is:global>
article {
//padding: 0 0.75rem;
& > h1:first-of-type {
display: none;
}

View file

@ -1,14 +1,30 @@
---
import BlogLayout from "../../layouts/BlogLayout.astro";
import ArticleCard from "../../components/ArticleCard.astro";
import { getCollection } from "astro:content";
const posts = await getCollection('blog');
const description =
"DarkCat09's blog about IT. " +
"Reading a QR code without smartphone, " +
"breaking a password on Windows, " +
"DarkCat09's blog. " +
"Reading a QR code without a phone, " +
"breaking Windows password, " +
"coding your own browser... " +
"The simpliest things!";
---
<BlogLayout title="Homepage" description={description}>
//
<div id="articles">
{posts.map(post => <ArticleCard post={post} />)}
</div>
</BlogLayout>
<style lang="less">
#articles {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.8rem;
}
</style>

View file

@ -7,5 +7,5 @@ const description =
---
<BlogLayout title="Blog | Shorts" description={description}>
//
// soon
</BlogLayout>

View file

@ -23,15 +23,15 @@ const description =
<Profile app="Discord" icon="&#xf392;" name="DarkCat09#5587" url="https://discord.com/app" copyName />
</section>
</article>
<article class="card" id="about">
<article class="card card-dashed" id="about">
<ul>
<ListItem icon="&#xf05a;">
<a href="https://t.me/dc09about" target="_blank">
https://t.me/dc09about
</a>
</ListItem>
<ListItem icon="&#xf5a0;">
Russia, Ulyanovsk
<ListItem icon="&#xf5a0;"><!-- Clock icon: &#xf017; -->
Russia, GMT+4
</ListItem>
<ListItem icon="&#xf025;">
Favorite music bands:

View file

@ -3,5 +3,5 @@ import MainLayout from "../layouts/MainLayout.astro";
---
<MainLayout page="Projects & Skills" description="">
//
// soon
</MainLayout>

View file

@ -7,5 +7,5 @@ const description =
---
<MainLayout page="Services" description={description}>
//
// soon
</MainLayout>

46
src/post-renderer.mts Normal file
View file

@ -0,0 +1,46 @@
import { AstroComponentFactory } from "astro/dist/runtime/server"
import { MarkdownHeading } from "astro"
export type postType = {
id: string
slug: string
body: string
collection: "blog"
data: any
} & {
render(): Promise<{
Content: AstroComponentFactory
headings: MarkdownHeading[]
remarkPluginFrontmatter: Record<string, any>
}>
}
export default async function postRenderer(post: postType) {
const postObj = await post.render()
const fm = postObj.remarkPluginFrontmatter
const title = postObj.headings[0]?.text || ''
const description: string = fm.description
const readingTime: number = fm.readingTime
const dateStr = post.slug.split("-", 1)[0]
let date: Date | undefined
if (!isNaN(Number(dateStr))) {
date = new Date(
`${dateStr.slice(0,4)}-` +
`${dateStr.slice(4,6)}-` +
`${dateStr.slice(6,8)}`
)
}
const image = (post.body.match(/(?:\s|^)!\[.*\]\((.*?)\)/) || [])[1]
return {
PostContent: postObj.Content,
title: title,
description: description,
readingTime: readingTime,
date: date,
image: image,
}
}