Compare commits
5 Commits
14bbd44d84
...
1d1e68241f
Author | SHA1 | Date | |
---|---|---|---|
1d1e68241f | |||
21cbbd8a97 | |||
50e084e464 | |||
4532101db1 | |||
3433ed7155 |
.vscode
DockerfileREADME.mdastro.config.tsoptions.tspackage-lock.jsonpackage.jsonplugins
src
tsconfig.json
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@ -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",
|
||||
|
@ -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
|
||||
|
||||
|
@ -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: {
|
||||
|
@ -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
2832
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@ -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",
|
||||
|
@ -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,
|
||||
},
|
||||
];
|
||||
}
|
||||
};
|
||||
|
@ -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',
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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 })}
|
||||
/>
|
||||
|
@ -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} />
|
||||
|
@ -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,
|
||||
};
|
@ -8,7 +8,7 @@ cover: /images/2024/11/2024112723215500.jpg
|
||||
published: true
|
||||
---
|
||||
|
||||

|
||||
[](/images/2024/11/2024112723242700.jpg)
|
||||
|
||||
<MusicPlayer netease={22705492} />
|
||||
|
||||
@ -38,4 +38,4 @@ published: true
|
||||
|
||||
雨帆就是我,一直很年轻的孩子。
|
||||
|
||||

|
||||
[](/images/2024/11/2024112723270800.jpg)
|
||||
|
@ -8,7 +8,7 @@ cover: /images/2024/11/2024112723314900.jpg
|
||||
published: true
|
||||
---
|
||||
|
||||

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

|
||||
[](/images/2024/11/2024112723400500.jpg)
|
||||
|
@ -9,7 +9,7 @@ friend: true
|
||||
published: true
|
||||
---
|
||||
|
||||

|
||||
[](/images/2024/11/2024112723183300.jpg)
|
||||
|
||||
<MusicPlayer netease={28306936} />
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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/".');
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -4,7 +4,7 @@ import Fuse from 'fuse.js';
|
||||
interface PostItem {
|
||||
title: string;
|
||||
slug: string;
|
||||
raw: string;
|
||||
raw: string | undefined;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user