Compare commits

...

16 commits

21 changed files with 1861 additions and 741 deletions

1
.gitignore vendored
View file

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

View file

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

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": "^2.3.0",
"astro-compress": "^1.1.42", "astro-compress": "^1.1.42",
"less": "^4.1.3", "less": "^4.1.3",
"mdast-util-to-string": "^3.2.0",
"reading-time": "^1.5.0", "reading-time": "^1.5.0",
"rehype-autolink-headings": "^6.1.1", "rehype-autolink-headings": "^6.1.1",
"rehype-figure": "^1.0.1", "rehype-figure": "^1.0.1",
"rehype-slug": "^5.1.0", "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 { select } from 'unist-util-select'
import { toString as mdToString } from 'mdast-util-to-string'; 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() { const firstParaText = mdToString(firstPara)
return function (tree, { data }) { const articleText = mdToString(tree)
const textOnPage = mdToString(tree);
const readingTime = getReadingTime(textOnPage); const readingTime = getReadingTime(articleText)
data.astro.frontmatter.readingTime = readingTime.text; data.astro.frontmatter.readingTime = readingTime.minutes
const description = textOnPage.slice(0, 100) + ELLIPSIS; data.astro.frontmatter.description = firstParaText
data.astro.frontmatter.description = description; }
}; }
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; name: string;
link: string; link: string;
icon: string; icon: string;
atLeft?: boolean;
} }
const { name, link, icon } = Astro.props; const { name, link, icon, atLeft = true } = Astro.props;
--- ---
<li> <li>
<slot name="js-dataset"></slot>
<a href={link}> <a href={link}>
<span class="icon-wrapper"> {!atLeft ? name : ""}
<span class:list={["icon-wrapper", {"highlight-icon": name == ""}]}>
<Icon code={icon} /> <Icon code={icon} />
</span> </span>
{name} {atLeft ? name : ""}
</a> </a>
</li> </li>
<style lang="less"> <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 { a {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
font-size: 1.15rem;
} }
.icon-wrapper { .icon-wrapper {
margin-right: 0.25rem; margin-right: 0.2rem;
width: 1.5rem;
font-size: 1.5rem; font-size: 1.5rem;
color: var(--fg-sec); color: var(--fg-sec);
&.highlight-icon:hover {
color: var(--fg);
}
} }
</style> </style>

View file

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

View file

@ -14,9 +14,10 @@ const { title, description } = Astro.props;
<Layout title={"Blog | " + title} description={description}> <Layout title={"Blog | " + title} description={description}>
<slot name="metadata" slot="metadata" /> <slot name="metadata" slot="metadata" />
<Menu> <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="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" /> <MenuItem name="Search" icon="&#xf002;" link="/blog/search" />
</Menu> </Menu>
<main> <main>

View file

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

View file

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

View file

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

View file

@ -1,24 +1,9 @@
--- ---
import BlogLayout from "../../layouts/BlogLayout.astro"; import BlogLayout from "../../layouts/BlogLayout.astro";
import Article from "../../components/Article.astro";
import { getCollection } from "astro:content"; import { getCollection } from "astro:content";
import postRenderer from "../../post-renderer.mjs";
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>;
}>;
};
export async function getStaticPaths() { export async function getStaticPaths() {
const posts = await getCollection("blog"); const posts = await getCollection("blog");
@ -31,29 +16,14 @@ export async function getStaticPaths() {
} }
const { slug } = Astro.params; const { slug } = Astro.params;
const post: postType = Astro.props.post; const { post } = Astro.props;
const postObj = await post.render(); const {
const fm = postObj.remarkPluginFrontmatter; PostContent, title, description,
readingTime, date, image,
} = await postRenderer(post);
const title = postObj.headings[0]?.text || ''; const locale = 'ru-RU';
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];
--- ---
<BlogLayout title={title} description={description}> <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:description" content={description} />
<meta property="twitter:image" content={image} /> <meta property="twitter:image" content={image} />
</Fragment> </Fragment>
<header> <Article
<h1>{title}</h1> PostContent={PostContent}
<address> title={title}
on description={description}
<time datetime={date?.toISOString()}>{date?.toLocaleDateString()}</time> readingTime={readingTime}
by date={date}
<a href="https://t.me/dcat09" rel="author">DarkCat09</a> image={image}
// />
<span id="reading-time">
{readingTime}
</span>
</address>
</header>
<article>
<postObj.Content />
</article>
</BlogLayout> </BlogLayout>
<style lang="less" is:global> <style lang="less" is:global>
article { article {
//padding: 0 0.75rem;
& > h1:first-of-type { & > h1:first-of-type {
display: none; display: none;
} }

View file

@ -1,14 +1,30 @@
--- ---
import BlogLayout from "../../layouts/BlogLayout.astro"; import BlogLayout from "../../layouts/BlogLayout.astro";
import ArticleCard from "../../components/ArticleCard.astro";
import { getCollection } from "astro:content";
const posts = await getCollection('blog');
const description = const description =
"DarkCat09's blog about IT. " + "DarkCat09's blog. " +
"Reading a QR code without smartphone, " + "Reading a QR code without a phone, " +
"breaking a password on Windows, " + "breaking Windows password, " +
"coding your own browser... " + "coding your own browser... " +
"The simpliest things!"; "The simpliest things!";
--- ---
<BlogLayout title="Homepage" description={description}> <BlogLayout title="Homepage" description={description}>
// <div id="articles">
{posts.map(post => <ArticleCard post={post} />)}
</div>
</BlogLayout> </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}> <BlogLayout title="Blog | Shorts" description={description}>
// // soon
</BlogLayout> </BlogLayout>

View file

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

View file

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

View file

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