Post rendering -> module; articles list on /blog; enhanced MenuItem
This commit is contained in:
parent
f325cebda0
commit
994a41f9e3
6 changed files with 252 additions and 66 deletions
44
src/components/Article.astro
Normal file
44
src/components/Article.astro
Normal 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>
|
||||||
|
}
|
113
src/components/ArticleCard.astro
Normal file
113
src/components/ArticleCard.astro
Normal 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=""
|
||||||
|
atLeft={false}
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
name={/*Copy Link*/""}
|
||||||
|
link="javascript:void(0)"
|
||||||
|
icon=""
|
||||||
|
atLeft={false}>
|
||||||
|
<span slot="js-dataset"
|
||||||
|
data-btn="copylink"
|
||||||
|
data-url={link}>
|
||||||
|
</span>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
name={/*Share*/""}
|
||||||
|
link="javascript:void(0)"
|
||||||
|
icon=""
|
||||||
|
atLeft={false}>
|
||||||
|
<span slot="js-dataset"
|
||||||
|
data-btn="share"
|
||||||
|
data-title={title}
|
||||||
|
data-url={link}>
|
||||||
|
</span>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
name="Read"
|
||||||
|
link={link}
|
||||||
|
icon=""
|
||||||
|
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>
|
|
@ -5,35 +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 {
|
|
||||||
font-size: 1.15rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
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.2rem;
|
margin-right: 0.2rem;
|
||||||
font-size: 1.5rem;
|
|
||||||
width: 1.5rem;
|
width: 1.5rem;
|
||||||
|
|
||||||
|
font-size: 1.5rem;
|
||||||
color: var(--fg-sec);
|
color: var(--fg-sec);
|
||||||
|
|
||||||
|
&.highlight-icon:hover {
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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. " +
|
"DarkCat09's blog. " +
|
||||||
"Reading a QR code without a phone, " +
|
"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}>
|
||||||
// soon
|
<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>
|
||||||
|
|
46
src/post-renderer.mts
Normal file
46
src/post-renderer.mts
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue