Compare commits

...

5 Commits

26 changed files with 1288 additions and 1997 deletions

View File

@ -4,7 +4,7 @@
"editor.codeActionsOnSave": {
"quickfix.biome": "explicit"
},
"astro.content-intellisense": true,
"astro.content-intellisense": false,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
@ -108,10 +108,12 @@
"owspace",
"pandiyan",
"penheulim",
"photoswipe",
"pilgi",
"plaiceholder",
"playform",
"psql",
"pswp",
"pwsz",
"qrcode",
"quan",

View File

@ -196,7 +196,7 @@ to get it worked everywhere.
This weblog is deployed on the [zeabur](https://zeabur.com) platform.
You can check their documents and get your own weblog to be published without any budget at first.
Or you can host on your own machine. Use [Dockerfile](./docs/Dockerfile) to build an image and run it locally.
Or you can host on your own machine. Use [Dockerfile](./Dockerfile) to build an image and run it locally.
The comment system is leverage the [Artalk](https://artalk.js.org), a self-hosted comment system.
You should host it on your own machine.
@ -222,8 +222,8 @@ You should host it on your own machine.
The source code of this blog is licensed under the [MIT](LICENSE) license,
feel to free to use it without any legal risks.
The [content](src/content) of this blog's posts is licensed under the
[CC BY-NC-SA 4.0](src/content/LICENSE) license.
The [content](content) of this blog's posts is licensed under the
[CC BY-NC-SA 4.0](content/LICENSE) license.
### Logo Fonts License

View File

@ -1,5 +1,5 @@
import mdx from '@astrojs/mdx';
import zeabur from '@zeabur/astro-adapter/serverless';
import node from '@astrojs/node';
import { uploader } from 'astro-uploader';
import { defineConfig, envField } from 'astro/config';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
@ -21,28 +21,24 @@ export default defineConfig({
domains: ['localhost', '127.0.0.1'],
service: !options.isProd() ? { entrypoint: './plugins/resize', config: {} } : undefined,
},
experimental: {
contentLayer: true,
contentIntellisense: true,
env: {
schema: {
// Postgres Database
POSTGRES_HOST: envField.string({ context: 'server', access: 'secret' }),
POSTGRES_PORT: envField.number({ context: 'server', access: 'secret' }),
POSTGRES_USERNAME: envField.string({ context: 'server', access: 'secret' }),
POSTGRES_PASSWORD: envField.string({ context: 'server', access: 'secret' }),
POSTGRES_DATABASE: envField.string({ context: 'server', access: 'secret' }),
// Artalk Comment
ARTALK_SCHEME: envField.string({ context: 'server', access: 'secret' }),
ARTALK_HOST: envField.string({ context: 'server', access: 'secret' }),
ARTALK_PORT: envField.number({ context: 'server', access: 'secret' }),
// Build the Open Graph
BUILD_OPEN_GRAPH: envField.boolean({ context: 'server', access: 'public', default: true }),
// Upload the files
UPLOAD_STATIC_FILES: envField.boolean({ context: 'server', access: 'public', default: false }),
},
validateSecrets: true,
env: {
schema: {
// Postgres Database
POSTGRES_HOST: envField.string({ context: 'server', access: 'secret' }),
POSTGRES_PORT: envField.number({ context: 'server', access: 'secret' }),
POSTGRES_USERNAME: envField.string({ context: 'server', access: 'secret' }),
POSTGRES_PASSWORD: envField.string({ context: 'server', access: 'secret' }),
POSTGRES_DATABASE: envField.string({ context: 'server', access: 'secret' }),
// Artalk Comment
ARTALK_SCHEME: envField.string({ context: 'server', access: 'secret' }),
ARTALK_HOST: envField.string({ context: 'server', access: 'secret' }),
ARTALK_PORT: envField.number({ context: 'server', access: 'secret' }),
// Build the Open Graph
BUILD_OPEN_GRAPH: envField.boolean({ context: 'server', access: 'public', default: true }),
// Upload the files
UPLOAD_STATIC_FILES: envField.boolean({ context: 'server', access: 'public', default: false }),
},
validateSecrets: true,
},
integrations: [
mdx({
@ -68,7 +64,9 @@ export default defineConfig({
}),
openGraph(),
],
adapter: zeabur(),
adapter: node({
mode: 'standalone',
}),
markdown: {
gfm: true,
shikiConfig: {

View File

@ -145,10 +145,10 @@ const options: z.input<typeof Options> = {
link: 'https://github.com/syhily',
},
{
name: '知乎',
icon: 'icon-zhihu-square-fill',
name: 'Follow',
icon: '<svg viewBox="0 0 83 80"><g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g transform="translate(0.6377, 0.6948)" fill="#FFFFFF" fill-rule="nonzero"><path d="M70.9713,-7.10542736e-15 L20.9446,-7.10542736e-15 C14.895,-7.10542736e-15 9.9907,4.8998 9.9907,10.944 C9.9907,16.9882 14.895,21.888 20.9446,21.888 L70.9713,21.888 C77.0213,21.888 81.9253,16.9882 81.9253,10.944 C81.9253,4.8998 77.0213,-7.10542736e-15 70.9713,-7.10542736e-15 Z" id="Path"></path><path d="M44.2971,28.4541 L10.9539,28.4541 C4.9042,28.4541 -7.10542736e-15,33.3539 -7.10542736e-15,39.3981 C-7.10542736e-15,45.4423 4.9042,50.3421 10.9539,50.3421 L44.2971,50.3421 C50.3468,50.3421 55.2511,45.4423 55.2511,39.3981 C55.2511,33.3539 50.3468,28.4541 44.2971,28.4541 Z" id="Path"></path><path d="M47.5231,67.8762 C47.5231,61.8324 42.6188,56.9326 36.5691,56.9326 C30.5195,56.9326 25.6152,61.8324 25.6152,67.8762 C25.6152,73.9212 30.5195,78.8202 36.5691,78.8202 C42.6188,78.8202 47.5231,73.9212 47.5231,67.8762 Z" id="Path"></path></g></g></svg>',
type: 'link',
link: 'https://www.zhihu.com/people/syhily',
link: 'https://app.follow.is/share/feeds/54772566650461214',
},
{
name: '微信',

2832
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -42,12 +42,13 @@
]
},
"dependencies": {
"@astrojs/mdx": "^3.1.9",
"@astrojs/mdx": "^4.0.1",
"@astrojs/node": "^9.0.0",
"@astrojs/rss": "^4.0.9",
"@zeabur/astro-adapter": "^1.0.6",
"astro": "^4.16.16",
"drizzle-orm": "^0.36.4",
"astro": "^5.0.1",
"drizzle-orm": "^0.37.0",
"fuse.js": "^7.0.0",
"glob": "^11.0.0",
"lodash": "^4.17.21",
"luxon": "^3.5.0",
"marked": "^15.0.3",
@ -60,7 +61,7 @@
"devDependencies": {
"@astrojs/check": "^0.9.4",
"@biomejs/biome": "^1.9.4",
"@napi-rs/canvas": "^0.1.64",
"@napi-rs/canvas": "^0.1.65",
"@types/lodash": "^4.17.13",
"@types/luxon": "^3.4.2",
"@types/node": "^22.10.1",
@ -68,8 +69,10 @@
"@types/qrcode-svg": "^1.1.5",
"@types/unist": "^3.0.3",
"aplayer": "^1.10.1",
"astro-uploader": "^1.2.1",
"astro-uploader": "^1.2.2",
"bootstrap": "^5.3.3",
"photoswipe": "^5.4.4",
"photoswipe-dynamic-caption-plugin": "^1.2.7",
"prettier": "^3.4.1",
"prettier-plugin-astro": "^0.14.1",
"prettier-plugin-astro-organize-imports": "^0.4.11",

View File

@ -1,9 +1,13 @@
import type { Literal, Node, Parent } from 'unist';
import { selectAll } from 'unist-util-select';
import options from '../options';
import { imageMetadata } from '../src/helpers/images';
import { urlJoin } from '../src/helpers/tools';
type LinkNode = Node & {
url: string;
children?: Node[];
};
type ImageNode = Parent & {
url: string;
alt: string;
@ -14,46 +18,33 @@ type ImageNode = Parent & {
};
export const astroImage = () => {
return async (tree: Node) => {
return (tree: Node) => {
// Find all the image node.
const imageNodes = selectAll('image', tree)
// Find all the image link nodes and replace the relative links.
selectAll('image', tree)
.map((node) => node as ImageNode)
.filter((imageNode) => !imageNode.url.startsWith('http'))
.map(transformAstroImage);
.filter((imageNode) => imageNode.url.startsWith('/'))
.map((imageNode) => {
imageNode.type = 'mdxJsxFlowElement';
imageNode.name = 'Image';
// Process image with blur metadata.
await Promise.all(imageNodes);
imageNode.attributes = [
{ type: 'mdxJsxAttribute', name: 'alt', value: imageNode.alt },
{ type: 'mdxJsxAttribute', name: 'src', value: imageNode.url },
{ type: 'mdxJsxAttribute', name: 'width', value: imageNode.width },
{ type: 'mdxJsxAttribute', name: 'height', value: imageNode.height },
];
});
for (const node of selectAll('link', tree)) {
const link = node as LinkNode;
if (link.children !== undefined && link.children.length !== 0) {
const images = link.children.filter((child) => child.type === 'image');
if (images.length > 0) {
link.url = link.url.startsWith('/') ? urlJoin(options.assetsPrefix(), link.url) : link.url;
}
}
}
return tree;
};
};
const transformAstroImage = async (imageNode: ImageNode) => {
imageNode.type = 'mdxJsxFlowElement';
imageNode.name = 'Image';
try {
const metadata = await imageMetadata(imageNode.url);
if (metadata == null) {
throw new Error(`Failed to get image metadata: ${imageNode.url}`);
}
imageNode.attributes = [
{ type: 'mdxJsxAttribute', name: 'alt', value: imageNode.alt },
{ type: 'mdxJsxAttribute', name: 'src', value: metadata.src },
{ type: 'mdxJsxAttribute', name: 'width', value: imageNode.width ?? metadata.width },
{ type: 'mdxJsxAttribute', name: 'height', value: imageNode.height ?? metadata.height },
{ type: 'mdxJsxAttribute', name: 'blurDataURL', value: metadata.blurDataURL },
{ type: 'mdxJsxAttribute', name: 'blurWidth', value: metadata.blurWidth },
{ type: 'mdxJsxAttribute', name: 'blurHeight', value: metadata.blurHeight },
];
} catch (error) {
imageNode.attributes = [
{ type: 'mdxJsxAttribute', name: 'alt', value: imageNode.alt },
{
type: 'mdxJsxAttribute',
name: 'src',
value: imageNode.url.startsWith('/') ? urlJoin(options.assetsPrefix(), imageNode.url) : imageNode.url,
},
];
}
};

View File

@ -66,8 +66,8 @@ const commentActions = {
content: z.string().min(1),
rid: z.number().optional(),
}),
handler: async (request) => {
const resp = await createComment(request);
handler: async (input, { request, clientAddress }) => {
const resp = await createComment(input, request, clientAddress);
if ('msg' in resp) {
throw new ActionError({
code: 'INTERNAL_SERVER_ERROR',

View File

@ -1,7 +1,42 @@
import Aplayer from 'aplayer/dist/APlayer.min.js';
import { actions, isInputError } from 'astro:actions';
import PhotoSwipe from 'photoswipe';
import PhotoSwipeDynamicCaption from 'photoswipe-dynamic-caption-plugin';
import PhotoSwipeLightbox from 'photoswipe/lightbox';
import stickySidebar from './sticky-sidebar.js';
// Lightbox support for post images.
const imageLinks = Array.from(document.querySelectorAll('.post-content a')).filter((link) => {
const img = link.querySelector('img');
return typeof img !== 'undefined' && img !== null;
});
if (imageLinks.length > 0) {
// Append the required data attributes.
for (const imageLink of imageLinks) {
const image = imageLink.querySelector('img');
if (image.getAttribute('width') !== null) {
imageLink.dataset.pswpWidth = image.getAttribute('width');
}
if (image.getAttribute('height') !== null) {
imageLink.dataset.pswpHeight = image.getAttribute('height');
}
}
const lightbox = new PhotoSwipeLightbox({
gallery: imageLinks,
showHideAnimationType: 'zoom',
showAnimationDuration: 300,
hideAnimationDuration: 300,
pswpModule: () => PhotoSwipe,
});
new PhotoSwipeDynamicCaption(lightbox, {
captionContent: (slide) => slide.data.alt,
});
lightbox.init();
}
// Error Popup.
const handleActionError = (error) => {
const errorMsg = isInputError(error)

View File

@ -548,6 +548,10 @@
justify-content: center;
}
.btn-icon span svg {
margin: 28%;
}
.btn-icon:hover,
.btn-icon:active,
.btn-icon:focus {
@ -579,39 +583,6 @@
font-size: 1.325rem;
}
.btn-icon .icon-status {
position: absolute;
left: 0;
top: 0;
z-index: 1;
}
.icon-status {
display: block;
font-size: 11px;
line-height: 1;
min-width: 20px;
margin: 1px;
padding: 2px 3px;
border-radius: var(--radius-lg);
color: #fff;
background-color: var(--bg-danger);
}
.icon-status.status-top {
left: 0;
top: 0;
}
.icon-status.status-right {
left: auto;
top: 0;
right: 0;
-webkit-transform: translate(50%, -50%);
-ms-transform: translate(50%, -50%);
transform: translate(50%, -50%);
}
@media (max-width: 767.98px) {
.btn-icon.btn-md {
width: 2.125rem;
@ -2251,6 +2222,7 @@ a:hover .overlay {
background-color: rgba(0, 0, 0, 0.9);
transition: all 0.2s ease-in-out;
transform: scale(0.8);
white-space: initial;
}
.nice-popup-error.error .icon {
@ -2494,6 +2466,10 @@ a:hover .overlay {
margin: 0 0 1rem 1rem;
}
.post-content a img {
cursor: zoom-in;
}
@media (max-width: 767.98px) {
.post-content h1,
.post-content h2,

View File

@ -46,8 +46,8 @@ const { comment, depth, pending } = Astro.props;
}
</div>
<div class="comment-content">
<Fragment set:html={comment.content} />
{pending && <p class="text-xs text-danger tip-comment-check">您的评论正在等待审核中...</p>}
<Fragment set:html={comment.content} />
</div>
<div class="comment-footer text-xs text-muted">
<time class="me-2">{formatLocalDate(comment.date, 'yyyy-MM-dd HH:mm')}</time>

View File

@ -47,27 +47,59 @@ export const increaseViews = async (key: string, title: string) => {
});
};
export const createComment = async (req: CommentReq): Promise<ErrorResp | CommentResp> => {
const user = await queryUser(req.email);
export const createComment = async (
commentReq: CommentReq,
req: Request,
clientAddress: string,
): Promise<ErrorResp | CommentResp> => {
const user = await queryUser(commentReq.email);
if (user !== null && user.name !== null) {
// Replace the comment user name for avoiding the duplicated users creation.
// We may add the commenter account management in the future.
req.name = user.name;
commentReq.name = user.name;
}
// Query the existing comments for the user.
const historicalParams = new URLSearchParams({
email: commentReq.email,
page_key: commentReq.page_key,
site_name: options.title,
flat_mode: 'true',
limit: '5',
sort_by: 'date_desc',
type: 'all',
}).toString();
const historicalComments = await fetch(urlJoin(server, `/api/v2/comments?${historicalParams}`), {
method: 'GET',
headers: {
'Content-type': 'application/json; charset=UTF-8',
},
})
.then(async (resp) => (await resp.json()).comments as Comment[])
.catch((e) => {
console.error(e);
return Array<Comment>();
});
if (historicalComments.find((comment) => comment.content === commentReq.content)) {
return { msg: '重复评论,你已经有了相同的留言,如果在页面看不到,说明它正在等待站长审核。' };
}
const response = await fetch(urlJoin(server, '/api/v2/comments'), {
method: 'POST',
headers: {
'User-Agent': req.headers.get('User-Agent') || 'node',
'X-Forwarded-For': clientAddress,
'Content-type': 'application/json; charset=UTF-8',
},
body: JSON.stringify({ ...req, site_name: options.title, rid: req.rid ? Number(req.rid) : 0 }),
body: JSON.stringify({ ...commentReq, site_name: options.title, rid: commentReq.rid ? Number(commentReq.rid) : 0 }),
}).catch((e) => {
console.error(e);
return null;
});
if (response === null) {
return { msg: 'failed to create comment' };
return { msg: '服务端异常,评论创建失败。' };
}
if (!response.ok) {

View File

@ -127,7 +127,11 @@ import options from '@/options';
class="btn btn-dark btn-icon btn-circle button-social"
>
<span>
<i class={`iconfont ${social.icon}`} />
{!social.icon.startsWith('<svg') ? (
<i class={`iconfont ${social.icon}`} />
) : (
<Fragment set:html={social.icon} />
)}
</span>
</a>
);

View File

@ -1,12 +1,22 @@
---
import { blurStyle, type Image } from '@/helpers/images';
import options from '@/options';
import { getEntry } from 'astro:content';
interface Props extends Image {
alt: string;
}
const { alt, src, width, height } = Astro.props;
let { alt, src, width, height, blurDataURL, blurWidth, blurHeight } = Astro.props;
if (src.startsWith('/')) {
const image = await getEntry('images', src);
src = image.data.src;
width = image.data.width;
height = image.data.height;
blurDataURL = image.data.blurDataURL;
blurWidth = image.data.blurWidth;
blurHeight = image.data.blurHeight;
}
---
<img
@ -15,5 +25,5 @@ const { alt, src, width, height } = Astro.props;
loading="lazy"
{width}
{height}
style={blurStyle(Astro.props)}
style={blurStyle({ src, width, height, blurDataURL, blurWidth, blurHeight })}
/>

View File

@ -1,11 +1,16 @@
---
import type { Image } from '@/helpers/images';
import { getEntry } from 'astro:content';
interface Props extends Image {
alt: string;
}
const { alt, src } = Astro.props;
let { alt, src } = Astro.props;
if (src.startsWith('/')) {
const image = await getEntry('images', src);
src = image.data.src;
}
---
<img {src} {alt} />

View File

@ -1,7 +1,10 @@
import { imageMetadata } from '@/helpers/images';
import { urlJoin } from '@/helpers/tools';
import options from '@/options';
import { glob } from 'astro/loaders';
import { defineCollection, z } from 'astro:content';
import { glob as Glob } from 'glob';
import path from 'node:path';
export const defaultCover = '/images/default-cover.jpg';
@ -15,12 +18,7 @@ const slug = () =>
.max(200)
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/i, 'Invalid slug');
const image = (fallbackImage: string) =>
z
.string()
.optional()
.default(fallbackImage)
.transform((file) => imageMetadata(file));
const image = (fallbackImage: string) => z.string().optional().default(fallbackImage);
// The default toc heading level.
const toc = () =>
@ -46,9 +44,30 @@ const toc = () =>
message: 'minHeadingLevel must be less than or equal to maxHeadingLevel',
});
// Images Collection
const imagesCollection = defineCollection({
loader: async () => {
const publicDirectory = path.join(process.cwd(), 'public');
const imagePaths = await Glob(path.join(publicDirectory, '**/*.{jpg,jpeg,gif,svg,png,webp}'));
const metas = imagePaths
.map((imagePath) => imagePath.substring(publicDirectory.length))
.map(async (imagePath) => ({ id: imagePath, ...(await imageMetadata(imagePath)) }));
return Promise.all(metas);
},
schema: z.object({
src: z.string(),
width: z.union([z.string(), z.number()]),
height: z.union([z.string(), z.number()]),
blurDataURL: z.string(),
blurWidth: z.number(),
blurHeight: z.number(),
}),
});
// Categories Collection
const categoriesCollection = defineCollection({
type: 'data',
loader: glob({ pattern: '**\/[^_]*.yml', base: './src/content/categories' }),
schema: z.object({
name: z.string().max(20),
slug: slug(),
@ -59,7 +78,7 @@ const categoriesCollection = defineCollection({
// Friends Collection
const friendsCollection = defineCollection({
type: 'data',
loader: glob({ pattern: '**\/[^_]*.yml', base: './src/content/friends' }),
schema: z.array(
z
.object({
@ -82,7 +101,7 @@ const friendsCollection = defineCollection({
// Posts Collection
const postsCollection = defineCollection({
type: 'content',
loader: glob({ pattern: '**\/[^_]*.mdx', base: './src/content/posts' }),
schema: z.object({
title: z.string().max(99),
date: z.date(),
@ -100,7 +119,7 @@ const postsCollection = defineCollection({
// Pages Collection
const pagesCollection = defineCollection({
type: 'content',
loader: glob({ pattern: '**\/[^_]*.mdx', base: './src/content/pages' }),
schema: z.object({
title: z.string().max(99),
date: z.date(),
@ -116,7 +135,7 @@ const pagesCollection = defineCollection({
// Tags Collection
const tagsCollection = defineCollection({
type: 'data',
loader: glob({ pattern: '**\/[^_]*.yml', base: './src/content/tags' }),
schema: z.array(
z.object({
name: z.string().max(20),
@ -131,4 +150,5 @@ export const collections = {
pages: pagesCollection,
posts: postsCollection,
tags: tagsCollection,
images: imagesCollection,
};

View File

@ -8,7 +8,7 @@ cover: /images/2024/11/2024112723215500.jpg
published: true
---
![ライブペイント by mocha@新刊委託中](/images/2024/11/2024112723242700.jpg)
[![ライブペイント by mocha@新刊委託中](/images/2024/11/2024112723242700.jpg)](/images/2024/11/2024112723242700.jpg)
<MusicPlayer netease={22705492} />
@ -38,4 +38,4 @@ published: true
雨帆就是我,一直很年轻的孩子。
![天使のはしご by mocha@新刊委託中](/images/2024/11/2024112723270800.jpg)
[![天使のはしご by mocha@新刊委託中](/images/2024/11/2024112723270800.jpg)](/images/2024/11/2024112723270800.jpg)

View File

@ -8,7 +8,7 @@ cover: /images/2024/11/2024112723314900.jpg
published: true
---
![春の陽 by 防人](/images/2024/11/2024112723303500.jpg)
[![春の陽 by 防人](/images/2024/11/2024112723303500.jpg)](/images/2024/11/2024112723303500.jpg)
<MusicPlayer netease={2166180181} />
@ -24,4 +24,4 @@ published: true
人生似飞花匆匆,飞花也有过绚丽。只是,当多年后的你再次看到这些留言时,是否能像飞花一样泰然。
![色なき風 by 防人](/images/2024/11/2024112723400500.jpg)
[![色なき風 by 防人](/images/2024/11/2024112723400500.jpg)](/images/2024/11/2024112723400500.jpg)

View File

@ -9,7 +9,7 @@ friend: true
published: true
---
![新作《天空的翅膀》 by 画师JW](/images/2024/11/2024112723183300.jpg)
[![新作《天空的翅膀》 by 画师JW](/images/2024/11/2024112723183300.jpg)](/images/2024/11/2024112723183300.jpg)
<MusicPlayer netease={28306936} />

View File

@ -1,5 +1,5 @@
import serverRenderer from '@astrojs/mdx/server.js';
import { experimental_AstroContainer as AstroContainer, type ContainerRenderOptions } from 'astro/container';
import serverRenderer from 'astro/jsx/server.js';
import type { AstroComponentFactory } from 'astro/runtime/server/index.js';
const container = await AstroContainer.create();

View File

@ -1,5 +1,6 @@
import fs from 'node:fs/promises';
import { join } from 'node:path';
import sharp from 'sharp';
import options from '../../options';
import { urlJoin } from './tools';
@ -54,8 +55,6 @@ export const blurStyle = (image: Image) => ({
// Copied and modified https://github.com/zce/velite/blob/main/src/assets.ts
export const imageMetadata = async (publicPath: string): Promise<Image> => {
const { default: sharp } = await import('sharp');
if (!publicPath.startsWith('/')) {
throw new Error('We only support image path in "public/images" directory. The path should start with "/images/".');
}

View File

@ -1,9 +1,11 @@
import { defaultCover } from '@/content/config.ts';
import { defaultCover } from '@/content.config';
import type { Image } from '@/helpers/images';
import options from '@/options';
import { getCollection, getEntry, type Render } from 'astro:content';
import { getCollection, getEntry, render, type RenderResult } from 'astro:content';
import { pinyin } from 'pinyin-pro';
// Import the collections from the astro content.
const imagesCollection = await getCollection('images');
const categoriesCollection = await getCollection('categories');
const friendsCollection = await getCollection('friends');
const pagesCollection = await getCollection('pages');
@ -11,57 +13,71 @@ const postsCollection = await getCollection('posts');
const tagsCollection = await getCollection('tags');
// Redefine the types from the astro content.
export type Category = (typeof categoriesCollection)[number]['data'] & {
export type Category = Omit<(typeof categoriesCollection)[number]['data'], 'cover'> & {
cover: Image;
counts: number;
permalink: string;
};
export type Friend = (typeof friendsCollection)[number]['data'][number];
export type Page = (typeof pagesCollection)[number]['data'] & {
export type Page = Omit<(typeof pagesCollection)[number]['data'], 'cover'> & {
cover: Image;
slug: string;
permalink: string;
render: () => Render['.mdx'];
render: () => Promise<RenderResult>;
};
export type Post = (typeof postsCollection)[number]['data'] & {
export type Post = Omit<(typeof postsCollection)[number]['data'], 'cover'> & {
cover: Image;
slug: string;
permalink: string;
render: () => Render['.mdx'];
raw: () => Promise<string>;
render: () => Promise<RenderResult>;
raw: () => Promise<string | undefined>;
};
export type Tag = (typeof tagsCollection)[number]['data'][number] & { counts: number; permalink: string };
// Translate the Astro content into the original content for dealing with different configuration types.
export const friends: Friend[] = friendsCollection[0].data;
export const friends: Friend[] = friendsCollection.flatMap((friends) => friends.data);
// Override the website for local debugging
export const pages: Page[] = pagesCollection
.filter((page) => page.data.published || !options.isProd())
.map((page) => ({
slug: page.slug,
permalink: `/${page.slug}`,
render: page.render,
...page.data,
}));
export const posts: Post[] = postsCollection
.filter((post) => post.data.published || !options.isProd())
.map((post) => ({
slug: post.slug,
permalink: `/posts/${post.slug}`,
render: post.render,
raw: async () => {
const entry = await getEntry('posts', post.slug);
return entry.body;
},
...post.data,
}))
.sort((left: Post, right: Post) => {
const a = left.date.getTime();
const b = right.date.getTime();
return options.settings.post.sort === 'asc' ? a - b : b - a;
});
export const categories: Category[] = categoriesCollection.map((cat) => ({
counts: posts.filter((post) => post.category === cat.data.name).length,
permalink: `/cats/${cat.data.slug}`,
...cat.data,
}));
export const pages: Page[] = await Promise.all(
pagesCollection
.filter((page) => page.data.published || !options.isProd())
.map(async (page) => ({
...page.data,
cover: (await getEntry('images', page.data.cover)).data,
slug: page.id,
permalink: `/${page.id}`,
render: async () => await render(await getEntry('pages', page.id)),
})),
);
export const posts: Post[] = (
await Promise.all(
postsCollection
.filter((post) => post.data.published || !options.isProd())
.map(async (post) => ({
...post.data,
cover: (await getEntry('images', post.data.cover)).data,
slug: post.id,
permalink: `/posts/${post.id}`,
render: async () => await render(await getEntry('posts', post.id)),
raw: async () => {
const entry = await getEntry('posts', post.id);
return entry.body;
},
})),
)
).sort((left: Post, right: Post) => {
const a = left.date.getTime();
const b = right.date.getTime();
return options.settings.post.sort === 'asc' ? a - b : b - a;
});
export const categories: Category[] = await Promise.all(
categoriesCollection.map(async (cat) => ({
...cat.data,
cover: (await getEntry('images', cat.data.cover)).data,
counts: posts.filter((post) => post.category === cat.data.name).length,
permalink: `/cats/${cat.data.slug}`,
})),
);
export const tags: Tag[] = tagsCollection.flatMap((tags) => {
return tags.data.map((tag) => ({
counts: posts.filter((post) => post.tags.includes(tag.name)).length,

View File

@ -4,7 +4,7 @@ import Fuse from 'fuse.js';
interface PostItem {
title: string;
slug: string;
raw: string;
raw: string | undefined;
tags: string[];
}

View File

@ -2,9 +2,11 @@
// tslint:disable:ordered-imports
import 'bootstrap/dist/css/bootstrap.min.css';
import '@/assets/styles/iconfont/iconfont.css';
import 'aplayer/dist/APlayer.min.css';
import '@/assets/styles/reset.css';
import '@/assets/styles/globals.css';
import 'aplayer/dist/APlayer.min.css';
import 'photoswipe/style.css';
import 'photoswipe-dynamic-caption-plugin/photoswipe-dynamic-caption-plugin.css';
import '@/assets/styles/opposans/opposans.css';
import Footer from '@/components/footer/Footer.astro';

View File

@ -1,6 +1,8 @@
{
"$schema": "https://json.schemastore.org/tsconfig.json",
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"],
"compilerOptions": {
"baseUrl": ".",
"strict": true,