feat: add upyun support. move images to upyun on build. (#45)
* style: better reply button. * feat: add astro badge. * chore: drop useless styles. * feat: use new options.ts for global configuration. * fix: the invalid import.meta.env.PROD in astro build. * feat: move og to new assert prefix. * chore: add better og description. * chore: make sure all the images link could be correctly replaced. * feat: add upyun uploader.
This commit is contained in:
parent
7fec4ba787
commit
0126b71722
27
.vscode/settings.json
vendored
27
.vscode/settings.json
vendored
@ -12,6 +12,10 @@
|
||||
},
|
||||
"cSpell.words": [
|
||||
"alexinea",
|
||||
"alignfull",
|
||||
"alignleft",
|
||||
"alignright",
|
||||
"alignwide",
|
||||
"ameho",
|
||||
"amehochan",
|
||||
"aplayer",
|
||||
@ -21,6 +25,7 @@
|
||||
"batang",
|
||||
"bigserial",
|
||||
"biomejs",
|
||||
"blogroll",
|
||||
"blogster",
|
||||
"blurhash",
|
||||
"captainofphb",
|
||||
@ -45,12 +50,13 @@
|
||||
"fong",
|
||||
"forencrypt",
|
||||
"giscus",
|
||||
"gogogo",
|
||||
"gotop",
|
||||
"gungseo",
|
||||
"hefei",
|
||||
"heiti",
|
||||
"HONORBKK",
|
||||
"HUAWEIYAL",
|
||||
"honorbkk",
|
||||
"huaweiyal",
|
||||
"ianvs",
|
||||
"iconfont",
|
||||
"iroha",
|
||||
@ -61,7 +67,7 @@
|
||||
"jing",
|
||||
"jungshik",
|
||||
"khalil",
|
||||
"KHTML",
|
||||
"khtml",
|
||||
"koanughi",
|
||||
"koaunghi",
|
||||
"lantinghei",
|
||||
@ -73,15 +79,17 @@
|
||||
"luxon",
|
||||
"mboker",
|
||||
"minagi",
|
||||
"Miui",
|
||||
"MMWEBID",
|
||||
"MMWEBSDK",
|
||||
"miui",
|
||||
"mmwebid",
|
||||
"mmwebsdk",
|
||||
"mochi",
|
||||
"napi",
|
||||
"netease",
|
||||
"nextval",
|
||||
"nian",
|
||||
"nocolor",
|
||||
"nofollow",
|
||||
"nopd",
|
||||
"noto",
|
||||
"oppo",
|
||||
"opposans",
|
||||
@ -95,7 +103,7 @@
|
||||
"qrcode",
|
||||
"quan",
|
||||
"recma",
|
||||
"Redmi",
|
||||
"redmi",
|
||||
"regclass",
|
||||
"sauvignon",
|
||||
"sheng",
|
||||
@ -104,6 +112,8 @@
|
||||
"shmily",
|
||||
"skrs",
|
||||
"syhily",
|
||||
"tabindex",
|
||||
"tagcloud",
|
||||
"taza",
|
||||
"teruteru",
|
||||
"timestamptz",
|
||||
@ -111,6 +121,7 @@
|
||||
"tsconfigs",
|
||||
"ultrahtml",
|
||||
"unpic",
|
||||
"upyun",
|
||||
"urlset",
|
||||
"varchar",
|
||||
"velite",
|
||||
@ -120,7 +131,7 @@
|
||||
"weibo",
|
||||
"xiao",
|
||||
"xinsenz",
|
||||
"XWEB",
|
||||
"xweb",
|
||||
"yefengs",
|
||||
"yetgul",
|
||||
"ying",
|
||||
|
@ -8,7 +8,7 @@ COPY . .
|
||||
ENV ASTRO_TELEMETRY_DISABLED=1
|
||||
RUN NODE_ENV=development npm install
|
||||
RUN npm i patch-package && npm exec patch-package
|
||||
RUN npm run build
|
||||
RUN NODE_ENV=production npm run build
|
||||
|
||||
FROM base AS runtime
|
||||
RUN npm install --omit=dev
|
||||
@ -16,4 +16,4 @@ COPY --from=build /app/dist ./dist
|
||||
ENV HOST=0.0.0.0
|
||||
ENV PORT=4321
|
||||
EXPOSE 4321
|
||||
CMD node ./dist/server/entry.mjs
|
||||
CMD NODE_ENV=production node ./dist/server/entry.mjs
|
||||
|
@ -1,15 +1,14 @@
|
||||
import mdx from '@astrojs/mdx';
|
||||
import node from '@astrojs/node';
|
||||
import { defineConfig, envField } from 'astro/config';
|
||||
import { astroImage } from './remark-plugins/images';
|
||||
|
||||
// Dynamic switch the site. This is hard coded.
|
||||
const port = 4321;
|
||||
const site = import.meta.env.PROD ? 'https://yufan.me' : `http://localhost:${port}`;
|
||||
import options from './options';
|
||||
import { astroImage } from './plugins/images';
|
||||
import { upyun } from './plugins/upyun';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: site,
|
||||
// This will override the import.meta.env.SITE. No need to introduce method.
|
||||
site: options.isProd() ? options.website : options.local.website,
|
||||
output: 'server',
|
||||
security: {
|
||||
checkOrigin: true,
|
||||
@ -17,11 +16,13 @@ export default defineConfig({
|
||||
experimental: {
|
||||
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_HOST: envField.string({ context: 'server', access: 'secret' }),
|
||||
},
|
||||
},
|
||||
@ -30,6 +31,9 @@ export default defineConfig({
|
||||
mdx({
|
||||
remarkPlugins: [astroImage],
|
||||
}),
|
||||
upyun({
|
||||
path: ['images', 'og', 'cats'],
|
||||
}),
|
||||
],
|
||||
adapter: node({
|
||||
mode: 'standalone',
|
||||
@ -42,8 +46,7 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
server: {
|
||||
host: true,
|
||||
port: port,
|
||||
port: options.local.port,
|
||||
},
|
||||
devToolbar: {
|
||||
// I don't need such toolbar.
|
||||
@ -53,4 +56,8 @@ export default defineConfig({
|
||||
// Add this for avoiding the needless import optimize in Vite.
|
||||
optimizeDeps: { exclude: ['@napi-rs/canvas'] },
|
||||
},
|
||||
build: {
|
||||
assets: 'cats',
|
||||
assetsPrefix: options.assetsPrefix(),
|
||||
},
|
||||
});
|
||||
|
177
options.ts
Normal file
177
options.ts
Normal file
@ -0,0 +1,177 @@
|
||||
import { z } from 'astro/zod';
|
||||
|
||||
// The type of the options, use zod for better validation.
|
||||
const Options = z
|
||||
.object({
|
||||
local: z
|
||||
.object({
|
||||
port: z.number(),
|
||||
})
|
||||
.transform((local) => ({ ...local, website: `http://localhost:${local.port}` })),
|
||||
title: z.string().max(40),
|
||||
website: z
|
||||
.string()
|
||||
.url()
|
||||
.refine((u) => !u.endsWith('/'))
|
||||
.readonly(),
|
||||
description: z.string().max(100),
|
||||
keywords: z.array(z.string()),
|
||||
author: z.object({ name: z.string(), email: z.string().email(), url: z.string().url() }),
|
||||
navigation: z.array(z.object({ text: z.string(), link: z.string(), target: z.string().optional() })),
|
||||
socials: z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
icon: z.string(),
|
||||
type: z.enum(['link', 'qrcode']),
|
||||
title: z.string().optional(),
|
||||
link: z.string().url(),
|
||||
}),
|
||||
),
|
||||
settings: z.object({
|
||||
initialYear: z.number().max(2024),
|
||||
icpNo: z.string().optional(),
|
||||
locale: z.string().optional().default('zh-CN'),
|
||||
timeZone: z.string().optional().default('Asia/Shanghai'),
|
||||
timeFormat: z.string().optional().default('yyyy-MM-dd HH:mm:ss'),
|
||||
twitter: z.string(),
|
||||
assetPrefix: z
|
||||
.string()
|
||||
.url()
|
||||
.refine((u) => !u.endsWith('/'))
|
||||
.readonly(),
|
||||
post: z.object({
|
||||
sort: z.enum(['asc', 'desc']),
|
||||
feature: z.array(z.string()).optional(),
|
||||
category: z.array(z.string()).optional(),
|
||||
}),
|
||||
pagination: z.object({
|
||||
posts: z.number().optional().default(5),
|
||||
category: z.number().optional().default(7),
|
||||
tags: z.number().optional().default(7),
|
||||
search: z.number().optional().default(7),
|
||||
}),
|
||||
feed: z.object({
|
||||
full: z.boolean().optional().default(true),
|
||||
size: z.number().optional().default(20),
|
||||
}),
|
||||
sidebar: z.object({
|
||||
search: z.boolean().default(false),
|
||||
post: z.number().default(6),
|
||||
comment: z.number().default(0),
|
||||
tag: z.number().default(20),
|
||||
}),
|
||||
comments: z.object({
|
||||
server: z.string().url().readonly(),
|
||||
admins: z.array(z.number()),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
.transform((opts) => {
|
||||
const isProd = (): boolean => import.meta.env.MODE === 'production' || process.env.NODE_ENV === 'production';
|
||||
const assetsPrefix = (): string => (isProd() ? opts.settings.assetPrefix : opts.local.website);
|
||||
return {
|
||||
...opts,
|
||||
// Monkey patch for the issue https://github.com/withastro/astro/issues/11282
|
||||
// No need to fallback to the import.meta.env.PROD I think.
|
||||
isProd,
|
||||
// Given that the import.meta.env.ASSETS_PREFIX has two types.
|
||||
// I have to use this uniform method instead.
|
||||
assetsPrefix,
|
||||
defaultOpenGraph: (): string => `${assetsPrefix()}/images/open-graph.png`,
|
||||
};
|
||||
});
|
||||
|
||||
const options = {
|
||||
local: {
|
||||
port: 4321,
|
||||
},
|
||||
title: '且听书吟',
|
||||
website: 'https://yufan.me',
|
||||
description: '诗与梦想的远方',
|
||||
keywords: ['雨帆', '且听书吟', 'syhily', 'amehochan', 'yufan'],
|
||||
author: {
|
||||
name: '雨帆',
|
||||
email: 'syhily@gmail.com',
|
||||
url: 'https://yufan.me',
|
||||
},
|
||||
navigation: [
|
||||
{
|
||||
text: '首页',
|
||||
link: '/',
|
||||
},
|
||||
{
|
||||
text: '关于',
|
||||
link: '/about',
|
||||
},
|
||||
{
|
||||
text: '留言',
|
||||
link: '/guestbook',
|
||||
},
|
||||
{
|
||||
text: '友链',
|
||||
link: '/links',
|
||||
},
|
||||
{
|
||||
text: '笔记',
|
||||
link: 'https://note.yufan.me',
|
||||
target: '_blank',
|
||||
},
|
||||
],
|
||||
socials: [
|
||||
{
|
||||
name: 'GitHub',
|
||||
icon: 'icon-github-fill',
|
||||
type: 'link',
|
||||
link: 'https://github.com/syhily',
|
||||
},
|
||||
{
|
||||
name: 'Twitter',
|
||||
icon: 'icon-twitter',
|
||||
type: 'link',
|
||||
link: 'https://twitter.com/amehochan',
|
||||
},
|
||||
{
|
||||
name: 'Wechat',
|
||||
icon: 'icon-wechat',
|
||||
type: 'qrcode',
|
||||
title: '扫码加我微信好友',
|
||||
link: 'https://u.wechat.com/EBpmuKmrVz4YVFnoCJdnruA',
|
||||
},
|
||||
],
|
||||
settings: {
|
||||
initialYear: 2011,
|
||||
icpNo: '皖ICP备2021002315号-2',
|
||||
locale: 'zh-CN',
|
||||
timeZone: 'Asia/Shanghai',
|
||||
timeFormat: 'yyyy-MM-dd',
|
||||
twitter: 'amehochan',
|
||||
assetPrefix: 'https://cat.yufan.me',
|
||||
post: {
|
||||
sort: 'desc',
|
||||
feature: ['secret-of-boys-mind', 'my-darling', 'happiness-caprice'],
|
||||
category: ['article', 'think', 'gossip', 'coding'],
|
||||
},
|
||||
pagination: {
|
||||
posts: 5,
|
||||
category: 7,
|
||||
tags: 7,
|
||||
search: 7,
|
||||
},
|
||||
feed: {
|
||||
full: true,
|
||||
size: 10,
|
||||
},
|
||||
sidebar: {
|
||||
search: true,
|
||||
post: 6,
|
||||
comment: 6,
|
||||
tag: 20,
|
||||
},
|
||||
comments: {
|
||||
server: 'https://comment.yufan.me',
|
||||
admins: [3],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default Options.parse(options);
|
227
package-lock.json
generated
227
package-lock.json
generated
@ -32,6 +32,7 @@
|
||||
"@types/pg": "^8.11.6",
|
||||
"@types/qrcode-svg": "^1.1.4",
|
||||
"@types/unist": "^3.0.2",
|
||||
"@types/upyun": "^3.4.3",
|
||||
"aplayer": "^1.10.1",
|
||||
"bootstrap": "^5.3.3",
|
||||
"prettier": "^3.3.2",
|
||||
@ -42,7 +43,8 @@
|
||||
"rimraf": "^5.0.7",
|
||||
"sharp": "^0.33.4",
|
||||
"typescript": "^5.4.5",
|
||||
"unist-util-visit": "^5.0.0"
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"upyun": "^3.4.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
@ -171,6 +173,29 @@
|
||||
"integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@astrojs/markdown-remark/node_modules/is-buffer": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
|
||||
"integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@astrojs/markdown-remark/node_modules/nlcst-to-string": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-3.1.1.tgz",
|
||||
@ -2774,6 +2799,16 @@
|
||||
"integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/upyun": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/upyun/-/upyun-3.4.3.tgz",
|
||||
"integrity": "sha512-iTZTvDxt1h4dmZeJnWsB7YeVRwTwEr8GE22LSKM3F464+4VnaUoIYtcwqkWnxEDPV97ZN3XA0BzSgCx84QR8jA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@ungap/structured-clone": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
|
||||
@ -3136,6 +3171,23 @@
|
||||
"sharp": "^0.33.3"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "0.26.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
|
||||
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.8"
|
||||
}
|
||||
},
|
||||
"node_modules/axobject-query": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz",
|
||||
@ -3414,6 +3466,16 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/charenc": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
|
||||
"integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
@ -3685,6 +3747,19 @@
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/comma-separated-tokens": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
|
||||
@ -3730,6 +3805,16 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/crypt": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
|
||||
"integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/cssesc": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||
@ -3772,6 +3857,16 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
@ -4349,6 +4444,27 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.6",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
||||
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/foreground-child": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz",
|
||||
@ -4366,6 +4482,21 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
@ -4812,6 +4943,13 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hmacsha1": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/hmacsha1/-/hmacsha1-1.0.0.tgz",
|
||||
"integrity": "sha512-4FP6J0oI8jqb6gLLl9tSwVdosWJ/AKSGJ+HwYf6Ixe4MUcEkst4uWzpVQrNOCin0fzTRQbXV8ePheU8WiiDYBw==",
|
||||
"dev": true,
|
||||
"license": "BSD"
|
||||
},
|
||||
"node_modules/html-escaper": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
|
||||
@ -4925,27 +5063,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/is-buffer": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
|
||||
"integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
|
||||
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-core-module": {
|
||||
"version": "2.13.1",
|
||||
@ -5084,6 +5206,13 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-promise": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
||||
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-reference": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz",
|
||||
@ -5393,6 +5522,18 @@
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/md5": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz",
|
||||
"integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"charenc": "0.0.2",
|
||||
"crypt": "0.0.2",
|
||||
"is-buffer": "~1.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-definitions": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz",
|
||||
@ -6464,6 +6605,29 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mimic-fn": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
|
||||
@ -8609,6 +8773,25 @@
|
||||
"browserslist": ">= 4.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/upyun": {
|
||||
"version": "3.4.6",
|
||||
"resolved": "https://registry.npmjs.org/upyun/-/upyun-3.4.6.tgz",
|
||||
"integrity": "sha512-ThAI7woGkVE2lsOq8MFYb0Oeg8avOQQbY3XmXmaq1aZVjzcglcMuI/RImBrq+KJw7nX39iNKCJKYs65xiAF53Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^0.26.1",
|
||||
"base-64": "^1.0.0",
|
||||
"form-data": "^4.0.0",
|
||||
"hmacsha1": "^1.0.0",
|
||||
"is-promise": "^4.0.0",
|
||||
"md5": "^2.3.0",
|
||||
"mime-types": "^2.1.15"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vfile": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz",
|
||||
|
@ -64,6 +64,7 @@
|
||||
"@types/pg": "^8.11.6",
|
||||
"@types/qrcode-svg": "^1.1.4",
|
||||
"@types/unist": "^3.0.2",
|
||||
"@types/upyun": "^3.4.3",
|
||||
"aplayer": "^1.10.1",
|
||||
"bootstrap": "^5.3.3",
|
||||
"prettier": "^3.3.2",
|
||||
@ -74,6 +75,7 @@
|
||||
"rimraf": "^5.0.7",
|
||||
"sharp": "^0.33.4",
|
||||
"typescript": "^5.4.5",
|
||||
"unist-util-visit": "^5.0.0"
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"upyun": "^3.4.6"
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ const transformAstroImage = async (imageNode: ImageNode) => {
|
||||
imageNode.name = 'Image';
|
||||
imageNode.attributes = [
|
||||
{ type: 'mdxJsxAttribute', name: 'alt', value: imageNode.alt },
|
||||
{ type: 'mdxJsxAttribute', name: 'src', value: imageNode.url },
|
||||
{ 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 },
|
89
plugins/upyun.ts
Normal file
89
plugins/upyun.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import type { AstroIntegration, AstroIntegrationLogger, RouteData } from 'astro';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import up from 'upyun';
|
||||
|
||||
export type UpyunOption = {
|
||||
path: string[];
|
||||
bucket?: string;
|
||||
operator?: string;
|
||||
password?: string;
|
||||
};
|
||||
|
||||
const defaultOption: UpyunOption = {
|
||||
path: ['images'],
|
||||
bucket: process.env.UPYUN_BUCKET,
|
||||
operator: process.env.UPYUN_OPERATOR,
|
||||
password: process.env.UPYUN_PASSWORD,
|
||||
};
|
||||
|
||||
export const upyun = (opt: UpyunOption): AstroIntegration => ({
|
||||
name: 'upyun',
|
||||
hooks: {
|
||||
'astro:build:done': async ({ dir, logger }: { dir: URL; routes: RouteData[]; logger: AstroIntegrationLogger }) => {
|
||||
const option: UpyunOption = { ...defaultOption, ...opt };
|
||||
if (typeof option.bucket === 'undefined' || opt.bucket === null) {
|
||||
logger.error('No "bucket" found on your configuration, skip deploying.');
|
||||
return;
|
||||
}
|
||||
if (typeof option.operator === 'undefined' || opt.operator === null) {
|
||||
logger.error('No "operator" found on your configuration, skip deploying.');
|
||||
return;
|
||||
}
|
||||
if (typeof option.password === 'undefined' || opt.password === null) {
|
||||
logger.error('No "password" found on your configuration, skip deploying.');
|
||||
return;
|
||||
}
|
||||
if (option.path.length === 0) {
|
||||
logger.warn('No files need to be upload to upyun. Skip.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create UPYUN Client
|
||||
const service = new up.Service(option.bucket, option.operator, option.password);
|
||||
const client = new up.Client(service);
|
||||
|
||||
// Upload one by one
|
||||
const staticRootPath = dir.pathname;
|
||||
for (const dir of option.path) {
|
||||
logger.info(`Start to upload the ${dir} to upyun`);
|
||||
await uploadFile(logger, client, staticRootPath, dir);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const normalizePath = (p: string): string => {
|
||||
return p.includes(path.win32.sep) ? p.split(path.win32.sep).join(path.posix.sep) : p;
|
||||
};
|
||||
|
||||
const uploadFile = async (logger: AstroIntegrationLogger, client: up.Client, root: string, current: string) => {
|
||||
const fullPath = path.join(root, current);
|
||||
const isDir = fs.statSync(fullPath).isDirectory();
|
||||
|
||||
// Visit file.
|
||||
if (!isDir) {
|
||||
const filePath = normalizePath(current);
|
||||
const res1 = await client.headFile(filePath);
|
||||
|
||||
if (res1 === false) {
|
||||
// This file need to be uploaded to upyun.
|
||||
// Try Create directory first.
|
||||
const newDir = filePath.substring(0, filePath.lastIndexOf(path.posix.sep));
|
||||
const res2 = await client.headFile(newDir);
|
||||
if (res2 === false) {
|
||||
logger.info(`Try to create ${newDir} on upyun`);
|
||||
await client.makeDir(newDir);
|
||||
}
|
||||
// Upload file.
|
||||
logger.info(`Try to upload file ${filePath} to upyun`);
|
||||
await client.putFile(filePath, fs.readFileSync(fullPath));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (const item of fs.readdirSync(fullPath)) {
|
||||
await uploadFile(logger, client, root, path.join(current, item));
|
||||
}
|
||||
};
|
File diff suppressed because one or more lines are too long
@ -1,3 +1,6 @@
|
||||
/*--------------------------------------------------------------
|
||||
color variables
|
||||
--------------------------------------------------------------*/
|
||||
:root {
|
||||
--color-primary: #008c95;
|
||||
--color-dark: #151b2b;
|
||||
@ -76,7 +79,6 @@
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
/* 0. CSS Reset
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
@ -190,7 +192,6 @@ table caption {
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
/* 1. Document Setup
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
|
@ -44,7 +44,7 @@ const { comment, config, depth, pending } = Astro.props;
|
||||
</div>
|
||||
<div class="comment-footer text-xs text-muted">
|
||||
<time class="me-2">{formatLocalDate(comment.date)}</time>
|
||||
<button class="text-secondary comment-reply-link" data-rid={comment.id}>回复</button>
|
||||
<button class="comment-reply-link" data-rid={comment.id}>回复</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
@ -8,8 +8,8 @@ import type {
|
||||
ErrorResp,
|
||||
} from '@/components/comment/types';
|
||||
import { increaseViews } from '@/helpers/db/query';
|
||||
import { options } from '@/helpers/schema';
|
||||
import { urlJoin } from '@/helpers/tools';
|
||||
import options from '@/options';
|
||||
import { ARTALK_HOST } from 'astro:env/server';
|
||||
import _ from 'lodash';
|
||||
import { marked } from 'marked';
|
||||
@ -18,7 +18,7 @@ import { ELEMENT_NODE, transform, walk } from 'ultrahtml';
|
||||
import sanitize from 'ultrahtml/transformers/sanitize';
|
||||
|
||||
// Access the artalk in internal docker host when it was deployed on zeabur.
|
||||
const server = import.meta.env.PROD ? `http://${ARTALK_HOST}:23366` : options.settings.comments.server;
|
||||
const server = options.isProd() ? `http://${ARTALK_HOST}:23366` : options.settings.comments.server;
|
||||
|
||||
export const getConfig = async (): Promise<CommentConfig | null> => {
|
||||
const data = await fetch(urlJoin(server, '/api/v2/conf'))
|
||||
|
@ -1,24 +1,42 @@
|
||||
---
|
||||
import options from '@/options';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { options } from '@/helpers/schema';
|
||||
|
||||
function currentYear(): number {
|
||||
return DateTime.now().setZone(options.settings.timeZone).year;
|
||||
}
|
||||
---
|
||||
|
||||
<footer class="border-top border-light text-xs text-center py-4 py-xl-5">
|
||||
Copyright © {options.settings.initialYear}-{currentYear()}{' '}
|
||||
<a href={import.meta.env.SITE} title={options.title} rel="home">
|
||||
{options.title}
|
||||
</a>
|
||||
<br />
|
||||
<footer class="footer border-top border-light text-xs text-center py-4 py-xl-5">
|
||||
<div class="line">
|
||||
<span>Copyright © {options.settings.initialYear}-{DateTime.now().setZone(options.settings.timeZone).year}</span>
|
||||
<a href={import.meta.env.SITE} title={options.title} rel="home">
|
||||
{options.title}
|
||||
</a>
|
||||
</div>
|
||||
{
|
||||
options.settings.icpNo && (
|
||||
<a href="https://beian.miit.gov.cn" rel="nofollow" target="_blank" title={'良民证'}>
|
||||
{options.settings.icpNo}
|
||||
</a>
|
||||
<div class="line">
|
||||
<a href="https://beian.miit.gov.cn" rel="nofollow" target="_blank" title="ICP 备案">
|
||||
{options.settings.icpNo}
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div class="line">
|
||||
<a href="https://astro.build" target="_blank" rel="nofollow"
|
||||
><svg class="astro-badge" fill="none" width="120" height="20" viewBox="0 0 120 20">
|
||||
<title>Astro Badge</title>
|
||||
<rect width="119" height="19" x=".5" y=".5" fill="#404b69" rx="3.5"></rect>
|
||||
<rect width="119" height="19" x=".5" y=".5" fill="url(#a)" fill-opacity=".5" rx="3.5"></rect>
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M9.07 13V6.79h1.86c1.44 0 2.14.54 2.14 1.63 0 .52-.16.98-.76 1.22v.25c.74.21.96.76.96 1.4 0 1.14-.72 1.71-2.15 1.71H9.07Zm.91-.8h1.14c.85 0 1.2-.29 1.2-1 0-.7-.36-.98-1.2-.98H9.98v1.98Zm0-2.75h.97c.84 0 1.2-.27 1.2-.93 0-.67-.35-.94-1.2-.94h-.97v1.87Zm6.69 3.7c-1.4 0-2.06-.8-2.06-2.45V6.8h.9v3.83c0 1.16.35 1.64 1.16 1.64.8 0 1.15-.48 1.15-1.64V6.79h.9v3.92c0 1.64-.66 2.43-2.05 2.43ZM20.2 13v-.83h1.58V7.62H20.2v-.83h4.09v.83H22.7v4.55h1.59V13H20.2Zm5.96 0V6.79h.9v5.38h2.86V13h-3.76Zm6.78 0V7.62h-1.77v-.83h4.45v.83h-1.76V13h-.92Zm9.57 0-.59-6.21h.88l.4 5.23.23.03L44 8.5h1.1l.57 3.55.25-.03.38-5.23h.88L46.59 13h-1.43l-.48-3.37h-.27L43.93 13H42.5Zm5.57 0v-.83h1.59V7.62h-1.59v-.83h4.09v.83h-1.58v4.55h1.58V13h-4.09Zm7.16 0V7.62h-1.76v-.83h4.45v.83h-1.77V13h-.92Zm7.2 0v-2.77h-2.32V13h-.91V6.79h.9V9.4h2.33V6.79h.9V13h-.9Zm10.38.87c-.5-.46-.64-1.4-.44-2.1.36.44.86.57 1.38.65.8.12 1.58.08 2.32-.29l.25-.15c.07.2.1.4.07.61-.06.5-.31.9-.71 1.19-.16.12-.33.22-.5.33-.5.34-.64.74-.45 1.33l.02.06a1.33 1.33 0 0 1-.6-.5c-.14-.24-.22-.5-.22-.78 0-.13 0-.27-.02-.4-.04-.33-.2-.48-.49-.48a.57.57 0 0 0-.6.46l-.01.07Zm-2.84-2.22s1.47-.71 2.95-.71l1.12-3.45c.04-.16.16-.28.3-.28.13 0 .26.12.3.28l1.11 3.45a6.3 6.3 0 0 1 2.96.71L76.2 4.83c-.07-.2-.2-.33-.36-.33h-3c-.17 0-.29.13-.36.33l-2.51 6.82Zm15.55-.99c0 .6-.75.97-1.8.97-.67 0-.91-.17-.91-.52 0-.37.3-.55.97-.55.61 0 1.14.01 1.74.09v.01Zm0-.74a7.85 7.85 0 0 0-1.6-.14c-1.95 0-2.87.46-2.87 1.54 0 1.11.63 1.54 2.09 1.54 1.23 0 2.06-.31 2.37-1.07h.05l-.02.5c0 .4.07.43.4.43h1.51c-.08-.23-.13-.9-.13-1.46 0-.61.03-1.08.03-1.7 0-1.26-.76-2.07-3.15-2.07-1.02 0-2.16.18-3.03.44.08.34.2 1.04.26 1.5a6.64 6.64 0 0 1 2.64-.51c1.14 0 1.46.26 1.46.79v.2Zm4.18 1.09c-.2.03-.49.03-.78.03-.3 0-.58 0-.77-.03l-.01.2c0 1.05.69 1.66 3.1 1.66 2.27 0 3-.6 3-1.66 0-1-.48-1.5-2.64-1.61-1.68-.08-1.82-.26-1.82-.47 0-.24.21-.37 1.34-.37 1.16 0 1.48.16 1.48.5v.07a16.91 16.91 0 0 1 1.55 0v-.2c0-1.23-1.02-1.63-3-1.63-2.23 0-2.99.55-2.99 1.61 0 .96.6 1.55 2.75 1.64 1.58.05 1.75.23 1.75.47 0 .26-.25.38-1.36.38-1.28 0-1.6-.18-1.6-.54v-.05Zm7.28-4.41a6.43 6.43 0 0 1-2.3 1.27c.02.31.02.88.02 1.2h.55l-.02 1.8c0 1.1.59 1.95 2.42 1.95a8.4 8.4 0 0 0 1.92-.22 16.4 16.4 0 0 1-.17-1.52c-.38.13-.86.2-1.39.2-.73 0-1.03-.2-1.03-.79V9.1c.95 0 1.9.02 2.45.04a18 18 0 0 1 .03-1.48l-2.45.02c0-.37.02-.72.03-1.07h-.06Zm4.94 2.21.02-1.16h-1.66a102.96 102.96 0 0 1 0 5.08h1.9a41.9 41.9 0 0 1-.04-2.08c0-1.14.46-1.46 1.51-1.46.49 0 .84.06 1.14.16.01-.42.1-1.25.14-1.62-.31-.1-.66-.15-1.08-.15-.9-.01-1.56.36-1.87 1.24l-.06-.01Zm8.27 1.34c0 .91-.66 1.34-1.7 1.34-1.03 0-1.7-.4-1.7-1.34 0-.94.68-1.29 1.7-1.29 1.03 0 1.7.38 1.7 1.3Zm1.73-.04c0-1.82-1.42-2.63-3.43-2.63-2.02 0-3.39.81-3.39 2.63 0 1.8 1.28 2.78 3.38 2.78 2.12 0 3.44-.97 3.44-2.78Z"
|
||||
></path>
|
||||
<rect width="119" height="19" x=".5" y=".5" stroke="url(#b)" rx="3.5"></rect>
|
||||
<defs>
|
||||
<linearGradient id="a" x1="0" x2="88.9" y1="20" y2="-43.52" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#404b69"></stop>
|
||||
<stop offset="1" stop-color="#404b69"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
|
@ -1,9 +0,0 @@
|
||||
<ul class="site-fixed-widget">
|
||||
<li class="fixed-gotop">
|
||||
<div class="btn btn-light btn-icon btn-lg btn-rounded btn-gotop">
|
||||
<span>
|
||||
<i class="iconfont icon-arrowup"></i>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
@ -3,7 +3,7 @@ import Logo from '@/components/header/Logo.astro';
|
||||
import LogoLarge from '@/components/header/LogoLarge.astro';
|
||||
import QRDialog from '@/components/image/QRDialog.astro';
|
||||
import SearchDialog from '@/components/search/SearchDialog.astro';
|
||||
import { options } from '@/helpers/schema';
|
||||
import options from '@/options';
|
||||
---
|
||||
|
||||
<header class="site-aside">
|
||||
|
@ -1,8 +1,8 @@
|
||||
---
|
||||
import { options } from '@/helpers/schema';
|
||||
import options from '@/options';
|
||||
---
|
||||
|
||||
<svg viewBox="0 0 300 300" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg viewBox="0 0 300 300">
|
||||
<title>{options.title}</title>
|
||||
<g id="logo-dark" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g transform="translate(9, 16)">
|
||||
|
@ -1,8 +1,8 @@
|
||||
---
|
||||
import { options } from '@/helpers/schema';
|
||||
import options from '@/options';
|
||||
---
|
||||
|
||||
<svg viewBox="0 0 1237 300" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg viewBox="0 0 1237 300">
|
||||
<title>{options.title}</title>
|
||||
<g id="logo-large" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="logo-white" transform="translate(20, 15)">
|
||||
|
@ -1,7 +1,8 @@
|
||||
---
|
||||
import QRDialog from '@/components/image/QRDialog.astro';
|
||||
import { options, type Post } from '@/helpers/schema';
|
||||
import type { Post } from '@/helpers/schema';
|
||||
import { urlJoin } from '@/helpers/tools';
|
||||
import options from '@/options';
|
||||
import * as querystring from 'node:querystring';
|
||||
|
||||
interface Props {
|
||||
|
@ -1,8 +1,7 @@
|
||||
---
|
||||
import { openGraphHeight, openGraphWidth } from '@/helpers/images';
|
||||
import { options } from '@/helpers/schema';
|
||||
import { getPageMeta } from '@/helpers/seo';
|
||||
import { urlJoin } from '@/helpers/tools';
|
||||
import options from '@/options';
|
||||
|
||||
export interface Props {
|
||||
title?: string;
|
||||
@ -16,12 +15,7 @@ const { og, twitter } = getPageMeta({
|
||||
title: title || options.title,
|
||||
description: description || options.description,
|
||||
baseUrl: import.meta.env.SITE,
|
||||
ogImageAbsoluteUrl:
|
||||
ogImageUrl === undefined
|
||||
? urlJoin(import.meta.env.SITE, '/images/open-graph.png')
|
||||
: ogImageUrl.startsWith('/')
|
||||
? urlJoin(import.meta.env.SITE, ogImageUrl)
|
||||
: ogImageUrl,
|
||||
ogImageUrl: ogImageUrl,
|
||||
ogImageAltText: title || options.title,
|
||||
ogImageWidth: openGraphWidth,
|
||||
ogImageHeight: openGraphHeight,
|
||||
|
@ -1,8 +1,7 @@
|
||||
---
|
||||
import { openGraphHeight, openGraphWidth } from '@/helpers/images';
|
||||
import { options } from '@/helpers/schema';
|
||||
import { getBlogPostMeta } from '@/helpers/seo';
|
||||
import { urlJoin } from '@/helpers/tools';
|
||||
import options from '@/options';
|
||||
|
||||
export interface Props {
|
||||
title?: string;
|
||||
@ -10,7 +9,7 @@ export interface Props {
|
||||
publishDate: string;
|
||||
requestPath: string;
|
||||
ogImageUrl?: string;
|
||||
ogImageAltText?: string;
|
||||
ogImageAltText: string;
|
||||
}
|
||||
|
||||
const { requestPath, title, description, publishDate, ogImageUrl, ogImageAltText } = Astro.props;
|
||||
@ -21,12 +20,7 @@ const { og, twitter } = getBlogPostMeta({
|
||||
pageUrl: requestPath.startsWith('http') ? requestPath : import.meta.env.SITE + requestPath,
|
||||
authorName: options.author.name,
|
||||
publishDate,
|
||||
ogImageAbsoluteUrl:
|
||||
typeof ogImageUrl === 'undefined'
|
||||
? urlJoin(import.meta.env.SITE, '/images/open-graph.png')
|
||||
: ogImageUrl.startsWith('/')
|
||||
? urlJoin(import.meta.env.SITE, ogImageUrl)
|
||||
: ogImageUrl,
|
||||
ogImageUrl: ogImageUrl,
|
||||
ogImageAltText,
|
||||
ogImageWidth: openGraphWidth,
|
||||
ogImageHeight: openGraphHeight,
|
||||
|
@ -1,6 +1,7 @@
|
||||
---
|
||||
import PinnedCategory from '@/components/page/category/PinnedCategory.astro';
|
||||
import { getCategory, options } from '@/helpers/schema';
|
||||
import { getCategory } from '@/helpers/schema';
|
||||
import options from '@/options';
|
||||
|
||||
const pinnedSlug = options.settings.post.category ?? [];
|
||||
const pinnedCategories = pinnedSlug
|
||||
|
@ -1,6 +1,7 @@
|
||||
---
|
||||
import FeaturePost from '@/components/page/post/FeaturePost.astro';
|
||||
import { options, type Post } from '@/helpers/schema';
|
||||
import type { Post } from '@/helpers/schema';
|
||||
import options from '@/options';
|
||||
|
||||
interface Props {
|
||||
posts: Post[];
|
||||
|
@ -2,7 +2,8 @@
|
||||
import Pagination from '@/components/page/pagination/Pagination.astro';
|
||||
import PostCards from '@/components/page/post/PostCards.astro';
|
||||
import { slicePosts } from '@/helpers/formatter';
|
||||
import { options, type Post } from '@/helpers/schema';
|
||||
import type { Post } from '@/helpers/schema';
|
||||
import options from '@/options';
|
||||
|
||||
interface Props {
|
||||
posts: Post[];
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
import { options } from '@/helpers/schema';
|
||||
import options from '@/options';
|
||||
---
|
||||
|
||||
<div id="search" class="widget widget_search" hidden={!options.settings.sidebar.search}>
|
||||
|
@ -1,7 +1,8 @@
|
||||
---
|
||||
import _ from 'lodash';
|
||||
|
||||
import { options, type Post } from '@/helpers/schema';
|
||||
import type { Post } from '@/helpers/schema';
|
||||
import options from '@/options';
|
||||
|
||||
interface Props {
|
||||
posts: Post[];
|
||||
|
@ -1,7 +1,8 @@
|
||||
---
|
||||
import _ from 'lodash';
|
||||
|
||||
import { options, type Tag } from '@/helpers/schema';
|
||||
import type { Tag } from '@/helpers/schema';
|
||||
import options from '@/options';
|
||||
|
||||
interface Props {
|
||||
tags: Tag[];
|
||||
|
@ -1,6 +1,6 @@
|
||||
---
|
||||
import { latestComments } from '@/helpers/db/query';
|
||||
import { options } from '@/helpers/schema';
|
||||
import options from '@/options';
|
||||
|
||||
const comments = await latestComments();
|
||||
---
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { imageMetadata } from '@/helpers/images';
|
||||
import { urlJoin } from '@/helpers/tools';
|
||||
import options from '@/options';
|
||||
import { defineCollection, z } from 'astro:content';
|
||||
|
||||
export const defaultCover = '/images/default-cover.jpg';
|
||||
@ -40,7 +42,9 @@ const friendsCollection = defineCollection({
|
||||
website: z.string().max(40),
|
||||
description: z.string().optional(),
|
||||
homepage: z.string().url(),
|
||||
poster: z.string(),
|
||||
poster: z
|
||||
.string()
|
||||
.transform((poster) => (poster.startsWith('/') ? urlJoin(options.assetsPrefix(), poster) : poster)),
|
||||
favicon: z.string().optional(),
|
||||
})
|
||||
.transform((data) => {
|
||||
@ -52,61 +56,6 @@ const friendsCollection = defineCollection({
|
||||
),
|
||||
});
|
||||
|
||||
// Options Collection
|
||||
const optionsCollection = defineCollection({
|
||||
type: 'data',
|
||||
schema: z.object({
|
||||
title: z.string().max(40),
|
||||
website: z.string().url(),
|
||||
description: z.string().max(100),
|
||||
keywords: z.array(z.string()),
|
||||
author: z.object({ name: z.string(), email: z.string().email(), url: z.string().url() }),
|
||||
navigation: z.array(z.object({ text: z.string(), link: z.string(), target: z.string().optional() })),
|
||||
socials: z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
icon: z.string(),
|
||||
type: z.enum(['link', 'qrcode']),
|
||||
title: z.string().optional(),
|
||||
link: z.string().url(),
|
||||
}),
|
||||
),
|
||||
settings: z.object({
|
||||
initialYear: z.number().max(2024),
|
||||
icpNo: z.string().optional(),
|
||||
locale: z.string().optional().default('zh-CN'),
|
||||
timeZone: z.string().optional().default('Asia/Shanghai'),
|
||||
timeFormat: z.string().optional().default('yyyy-MM-dd HH:mm:ss'),
|
||||
twitter: z.string(),
|
||||
post: z.object({
|
||||
sort: z.enum(['asc', 'desc']),
|
||||
feature: z.array(z.string()).optional(),
|
||||
category: z.array(z.string()).optional(),
|
||||
}),
|
||||
pagination: z.object({
|
||||
posts: z.number().optional().default(5),
|
||||
category: z.number().optional().default(7),
|
||||
tags: z.number().optional().default(7),
|
||||
search: z.number().optional().default(7),
|
||||
}),
|
||||
feed: z.object({
|
||||
full: z.boolean().optional().default(true),
|
||||
size: z.number().optional().default(20),
|
||||
}),
|
||||
sidebar: z.object({
|
||||
search: z.boolean().default(false),
|
||||
post: z.number().default(6),
|
||||
comment: z.number().default(0),
|
||||
tag: z.number().default(20),
|
||||
}),
|
||||
comments: z.object({
|
||||
server: z.string().url().readonly(),
|
||||
admins: z.array(z.number()),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
// Posts Collection
|
||||
const postsCollection = defineCollection({
|
||||
type: 'content',
|
||||
@ -151,7 +100,6 @@ const tagsCollection = defineCollection({
|
||||
export const collections = {
|
||||
categories: categoriesCollection,
|
||||
friends: friendsCollection,
|
||||
options: optionsCollection,
|
||||
pages: pagesCollection,
|
||||
posts: postsCollection,
|
||||
tags: tagsCollection,
|
||||
|
@ -1,92 +0,0 @@
|
||||
title: 且听书吟
|
||||
website: https://yufan.me
|
||||
description: 诗与梦想的远方
|
||||
keywords:
|
||||
- 雨帆
|
||||
- 且听书吟
|
||||
- syhily
|
||||
- amehochan
|
||||
- yufan
|
||||
author:
|
||||
name: 雨帆
|
||||
email: syhily@gmail.com
|
||||
url: https://yufan.me
|
||||
navigation:
|
||||
- text: 首页
|
||||
link: /
|
||||
- text: 关于
|
||||
link: /about
|
||||
- text: 留言
|
||||
link: /guestbook
|
||||
- text: 友链
|
||||
link: /links
|
||||
- text: 笔记
|
||||
link: https://note.yufan.me
|
||||
target: _blank
|
||||
socials:
|
||||
- name: GitHub
|
||||
icon: icon-github-fill
|
||||
type: link
|
||||
link: https://github.com/syhily
|
||||
- name: Twitter
|
||||
icon: icon-twitter
|
||||
type: link
|
||||
link: https://twitter.com/amehochan
|
||||
- name: Wechat
|
||||
icon: icon-wechat
|
||||
type: qrcode
|
||||
title: 扫码加我微信好友
|
||||
link: https://u.wechat.com/EBpmuKmrVz4YVFnoCJdnruA
|
||||
settings:
|
||||
initialYear: 2011
|
||||
icpNo: 皖ICP备2021002315号-2
|
||||
locale: zh-CN
|
||||
timeZone: Asia/Shanghai
|
||||
timeFormat: yyyy-MM-dd
|
||||
twitter: amehochan
|
||||
post:
|
||||
# asc or desc
|
||||
sort: desc
|
||||
# This is the PINGED posts on top of your homepage.
|
||||
# You can add the posts id here.
|
||||
# We only show the first three feature posts.
|
||||
feature:
|
||||
- secret-of-boys-mind
|
||||
- my-darling
|
||||
- happiness-caprice
|
||||
# The pinned category slugs.
|
||||
category:
|
||||
- article
|
||||
- think
|
||||
- gossip
|
||||
- coding
|
||||
pagination:
|
||||
# Number of posts per home page.
|
||||
posts: 5
|
||||
# Number of posts per category page.
|
||||
category: 7
|
||||
# Number of posts per tag page.
|
||||
tags: 7
|
||||
# Number of posts in search results.
|
||||
search: 7
|
||||
feed:
|
||||
# Whether to show the post's content.
|
||||
full: true
|
||||
# The counts of the latest post to show.
|
||||
size: 10
|
||||
sidebar:
|
||||
# Display search bar.
|
||||
search: true
|
||||
# Display the number of random posts. Set 0 or a negation number to disable it.
|
||||
post: 6
|
||||
# Display recent blog comments. Set 0 or a negative number to disable it.
|
||||
comment: 6
|
||||
# Display the tag cloud. Set 0 or a negative number to disable it.
|
||||
tag: 20
|
||||
# Artalk configuration
|
||||
comments:
|
||||
# The artalk server endpoint
|
||||
server: https://comment.yufan.me
|
||||
# The administrator user id.
|
||||
admins:
|
||||
- 3
|
@ -1,7 +1,7 @@
|
||||
import { db } from '@/helpers/db/pool';
|
||||
import { atk_comments, atk_likes, atk_pages, atk_users } from '@/helpers/db/schema';
|
||||
import { options } from '@/helpers/schema';
|
||||
import { makeToken, urlJoin } from '@/helpers/tools';
|
||||
import options from '@/options';
|
||||
import { and, desc, eq, isNull, notInArray, sql } from 'drizzle-orm';
|
||||
|
||||
export interface Comment {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { options, type Post } from '@/helpers/schema';
|
||||
import type { Post } from '@/helpers/schema';
|
||||
import options from '@/options';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
export const slicePosts = (
|
||||
|
@ -1,5 +1,7 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import options from '../../options';
|
||||
import { urlJoin } from './tools';
|
||||
|
||||
export interface Image {
|
||||
/**
|
||||
@ -55,7 +57,7 @@ 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 direct. It should start with "/".');
|
||||
throw new Error('We only support image path in "public/images" directory. The path should start with "/images/".');
|
||||
}
|
||||
|
||||
const root = join(process.cwd(), 'public');
|
||||
@ -71,5 +73,5 @@ export const imageMetadata = async (publicPath: string): Promise<Image> => {
|
||||
const blurImage = await img.resize(blurWidth, blurHeight).webp({ quality: 1 }).toBuffer();
|
||||
const blurDataURL = `data:image/webp;base64,${blurImage.toString('base64')}`;
|
||||
|
||||
return { src: publicPath, height, width, blurDataURL, blurWidth, blurHeight };
|
||||
return { src: urlJoin(options.assetsPrefix(), publicPath), height, width, blurDataURL, blurWidth, blurHeight };
|
||||
};
|
||||
|
@ -5,7 +5,7 @@
|
||||
* But I have get the approvement to use them here by asking the author https://twitter.com/yuaanlin.
|
||||
*/
|
||||
import { openGraphHeight, openGraphWidth } from '@/helpers/images';
|
||||
import { options } from '@/helpers/schema';
|
||||
import options from '@/options';
|
||||
import { Canvas, GlobalFonts, Image, type SKRSContext2D } from '@napi-rs/canvas';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
@ -117,6 +117,11 @@ const drawImageProp = (
|
||||
};
|
||||
|
||||
const fetchCover = async (cover: string): Promise<Buffer> => {
|
||||
if (cover.startsWith(options.assetsPrefix())) {
|
||||
const coverPath = join(process.cwd(), 'public', cover.substring(options.assetsPrefix().length));
|
||||
return await readFile(coverPath);
|
||||
}
|
||||
|
||||
if (cover.startsWith('http')) {
|
||||
return Buffer.from(await (await fetch(cover)).arrayBuffer());
|
||||
}
|
||||
@ -132,7 +137,7 @@ export interface OpenGraphProps {
|
||||
}
|
||||
|
||||
export const defaultOpenGraph = async (): Promise<Buffer> => {
|
||||
return await fetchCover('/images/default-cover.jpg');
|
||||
return await fetchCover('/images/open-graph.png');
|
||||
};
|
||||
|
||||
// Register the font if it doesn't exist
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { defaultCover } from '@/content/config.ts';
|
||||
import options from '@/options';
|
||||
import { getCollection, getEntryBySlug, type Render } from 'astro:content';
|
||||
|
||||
// Import the collections from the astro content.
|
||||
const categoriesCollection = await getCollection('categories');
|
||||
const friendsCollection = await getCollection('friends');
|
||||
const optionsCollection = await getCollection('options');
|
||||
const pagesCollection = await getCollection('pages');
|
||||
const postsCollection = await getCollection('posts');
|
||||
const tagsCollection = await getCollection('tags');
|
||||
@ -12,7 +12,6 @@ const tagsCollection = await getCollection('tags');
|
||||
// Redefine the types from the astro content.
|
||||
export type Category = (typeof categoriesCollection)[number]['data'] & { counts: number; permalink: string };
|
||||
export type Friend = (typeof friendsCollection)[number]['data'][number];
|
||||
export type Options = (typeof optionsCollection)[number]['data'];
|
||||
export type Page = (typeof pagesCollection)[number]['data'] & {
|
||||
slug: string;
|
||||
permalink: string;
|
||||
@ -28,10 +27,9 @@ export type Tag = (typeof tagsCollection)[number]['data'][number] & { counts: nu
|
||||
|
||||
// Translate the Astro content into the original content for dealing with different configuration types.
|
||||
export const friends: Friend[] = friendsCollection[0].data;
|
||||
export const options: Options = optionsCollection[0].data;
|
||||
// Override the website for local debugging
|
||||
export const pages: Page[] = pagesCollection
|
||||
.filter((page) => page.data.published || !import.meta.env.PROD)
|
||||
.filter((page) => page.data.published || !options.isProd())
|
||||
.map((page) => ({
|
||||
slug: page.slug,
|
||||
permalink: `/${page.slug}`,
|
||||
@ -42,7 +40,7 @@ export const pages: Page[] = pagesCollection
|
||||
...page.data,
|
||||
}));
|
||||
export const posts: Post[] = postsCollection
|
||||
.filter((post) => post.data.published || !import.meta.env.PROD)
|
||||
.filter((post) => post.data.published || !options.isProd())
|
||||
.map((post) => ({
|
||||
slug: post.slug,
|
||||
permalink: `/posts/${post.slug}`,
|
||||
@ -90,7 +88,7 @@ if (missingTags.length > 0) {
|
||||
const missingCovers = posts
|
||||
.filter((post) => post.cover.src === defaultCover)
|
||||
.map((post) => ({ title: post.title, slug: post.slug }));
|
||||
if (!import.meta.env.PROD && missingCovers.length > 0) {
|
||||
if (!options.isProd() && missingCovers.length > 0) {
|
||||
// We only warn here for this is a known improvement.
|
||||
console.warn(`The following ${missingCovers.length} posts don't have a cover.`);
|
||||
console.warn(missingCovers);
|
||||
@ -124,13 +122,6 @@ if (invalidPinnedCategories.length > 0) {
|
||||
throw new Error(`The bellowing pinned categories are invalid:\n$${invalidPinnedCategories.join('\n')}`);
|
||||
}
|
||||
|
||||
// Validate the options with the Astro configuration.
|
||||
if (import.meta.env.PROD && import.meta.env.SITE !== options.website) {
|
||||
throw new Error(
|
||||
`Invalid configuration in options.website: ${options.website} with astro site: ${import.meta.env.SITE}`,
|
||||
);
|
||||
}
|
||||
|
||||
export const getPost = (slug: string): Post | undefined => {
|
||||
return posts.find((post) => post.slug === slug);
|
||||
};
|
||||
|
@ -1,7 +1,45 @@
|
||||
// This file is copied from https://github.com/flexdinesh/blogster/blob/main/packages/shared/src/seo.ts
|
||||
// I just modified it for my personal needs.
|
||||
import { urlJoin } from '@/helpers/tools';
|
||||
import options from '@/options';
|
||||
|
||||
type PageOgMeta = {
|
||||
export interface PageMeta {
|
||||
title: string;
|
||||
description: string;
|
||||
baseUrl?: string;
|
||||
ogImageUrl?: string;
|
||||
ogImageAltText: string;
|
||||
ogImageWidth?: number;
|
||||
ogImageHeight?: number;
|
||||
siteOwnerTwitterHandle?: string;
|
||||
contentAuthorTwitterHandle?: string;
|
||||
}
|
||||
|
||||
export interface PostMeta {
|
||||
title: string;
|
||||
description: string;
|
||||
pageUrl?: string;
|
||||
authorName?: string;
|
||||
publishDate: string;
|
||||
ogImageUrl?: string;
|
||||
ogImageAltText: string;
|
||||
ogImageWidth?: number;
|
||||
ogImageHeight?: number;
|
||||
siteOwnerTwitterHandle?: string;
|
||||
contentAuthorTwitterHandle?: string;
|
||||
}
|
||||
|
||||
export interface TwitterOgMeta {
|
||||
title: string;
|
||||
description?: string;
|
||||
card: 'summary_large_image';
|
||||
site?: string;
|
||||
creator?: string;
|
||||
image?: string;
|
||||
imageAlt?: string;
|
||||
}
|
||||
|
||||
export interface PageOgMeta {
|
||||
title: string;
|
||||
description?: string;
|
||||
type: 'website';
|
||||
@ -10,19 +48,9 @@ type PageOgMeta = {
|
||||
imageAlt?: string;
|
||||
imageWidth?: string;
|
||||
imageHeight?: string;
|
||||
};
|
||||
}
|
||||
|
||||
type PageTwitterMeta = {
|
||||
title: string;
|
||||
description?: string;
|
||||
card: 'summary_large_image';
|
||||
site?: string;
|
||||
creator?: string;
|
||||
image?: string;
|
||||
imageAlt?: string;
|
||||
};
|
||||
|
||||
type PostOgMeta = {
|
||||
export interface PostOgMeta {
|
||||
title: string;
|
||||
description?: string;
|
||||
type: 'article';
|
||||
@ -34,130 +62,93 @@ type PostOgMeta = {
|
||||
imageAlt?: string;
|
||||
imageWidth?: string;
|
||||
imageHeight?: string;
|
||||
};
|
||||
}
|
||||
|
||||
type PostTwitterMeta = {
|
||||
title: string;
|
||||
description?: string;
|
||||
card: 'summary_large_image';
|
||||
site?: string;
|
||||
creator?: string;
|
||||
image?: string;
|
||||
imageAlt?: string;
|
||||
};
|
||||
const parseOgImageUrl = (ogImageUrl?: string): string =>
|
||||
typeof ogImageUrl === 'undefined'
|
||||
? options.defaultOpenGraph()
|
||||
: ogImageUrl.startsWith('/')
|
||||
? urlJoin(options.assetsPrefix(), ogImageUrl)
|
||||
: ogImageUrl;
|
||||
|
||||
export function getPageMeta({
|
||||
title: pageTitle,
|
||||
export const getPageMeta = ({
|
||||
title,
|
||||
description,
|
||||
baseUrl,
|
||||
ogImageAbsoluteUrl,
|
||||
ogImageUrl,
|
||||
ogImageAltText,
|
||||
ogImageWidth,
|
||||
ogImageHeight,
|
||||
siteOwnerTwitterHandle,
|
||||
contentAuthorTwitterHandle,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
baseUrl?: string;
|
||||
ogImageAbsoluteUrl?: string; // should always be absolute
|
||||
ogImageAltText?: string;
|
||||
ogImageWidth?: number;
|
||||
ogImageHeight?: number;
|
||||
siteOwnerTwitterHandle?: string;
|
||||
contentAuthorTwitterHandle?: string;
|
||||
}): { og: PageOgMeta; twitter: PageTwitterMeta } {
|
||||
if (!pageTitle) {
|
||||
}: PageMeta): { og: PageOgMeta; twitter: TwitterOgMeta } => {
|
||||
if (!title) {
|
||||
throw Error('title is required for page SEO');
|
||||
}
|
||||
if (ogImageAbsoluteUrl) {
|
||||
ogImageAltText = !ogImageAltText ? `Preview image for ${pageTitle}` : ogImageAltText;
|
||||
}
|
||||
|
||||
const og: PageOgMeta = {
|
||||
title: pageTitle,
|
||||
description: description,
|
||||
type: 'website',
|
||||
url: baseUrl,
|
||||
image: ogImageAbsoluteUrl,
|
||||
imageAlt: ogImageAltText,
|
||||
imageWidth: ogImageWidth ? String(ogImageWidth) : undefined,
|
||||
imageHeight: ogImageHeight ? String(ogImageHeight) : undefined,
|
||||
};
|
||||
|
||||
const twitter: PageTwitterMeta = {
|
||||
title: pageTitle,
|
||||
description: description,
|
||||
card: 'summary_large_image',
|
||||
site: siteOwnerTwitterHandle,
|
||||
creator: contentAuthorTwitterHandle || siteOwnerTwitterHandle,
|
||||
image: ogImageAbsoluteUrl,
|
||||
imageAlt: ogImageAltText,
|
||||
};
|
||||
const ogImageAbsoluteUrl = parseOgImageUrl(ogImageUrl);
|
||||
|
||||
return {
|
||||
og,
|
||||
twitter,
|
||||
og: {
|
||||
title: title,
|
||||
description: description,
|
||||
type: 'website',
|
||||
url: baseUrl,
|
||||
image: ogImageAbsoluteUrl,
|
||||
imageAlt: ogImageAltText,
|
||||
imageWidth: ogImageWidth ? String(ogImageWidth) : undefined,
|
||||
imageHeight: ogImageHeight ? String(ogImageHeight) : undefined,
|
||||
},
|
||||
twitter: {
|
||||
title: title,
|
||||
description: description,
|
||||
card: 'summary_large_image',
|
||||
site: siteOwnerTwitterHandle,
|
||||
creator: contentAuthorTwitterHandle || siteOwnerTwitterHandle,
|
||||
image: ogImageAbsoluteUrl,
|
||||
imageAlt: ogImageAltText,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export function getBlogPostMeta({
|
||||
title: pageTitle,
|
||||
export const getBlogPostMeta = ({
|
||||
title,
|
||||
description,
|
||||
pageUrl,
|
||||
authorName,
|
||||
publishDate,
|
||||
ogImageAbsoluteUrl,
|
||||
ogImageUrl,
|
||||
ogImageAltText,
|
||||
ogImageWidth,
|
||||
ogImageHeight,
|
||||
siteOwnerTwitterHandle,
|
||||
contentAuthorTwitterHandle,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
pageUrl?: string;
|
||||
authorName?: string;
|
||||
publishDate: string;
|
||||
ogImageAbsoluteUrl?: string; // should always be absolute
|
||||
ogImageAltText?: string;
|
||||
ogImageWidth?: number;
|
||||
ogImageHeight?: number;
|
||||
siteOwnerTwitterHandle?: string;
|
||||
contentAuthorTwitterHandle?: string;
|
||||
}): { og: PostOgMeta; twitter: PostTwitterMeta } {
|
||||
if (!pageTitle) {
|
||||
}: PostMeta): { og: PostOgMeta; twitter: TwitterOgMeta } => {
|
||||
if (!title) {
|
||||
throw Error('title is required for page SEO');
|
||||
}
|
||||
if (ogImageAbsoluteUrl && !ogImageAltText) {
|
||||
ogImageAltText = `Preview image for ${pageTitle}`;
|
||||
}
|
||||
|
||||
const og: PostOgMeta = {
|
||||
title: pageTitle,
|
||||
description: description,
|
||||
type: 'article',
|
||||
url: pageUrl,
|
||||
author: authorName,
|
||||
publishDate: publishDate,
|
||||
image: ogImageAbsoluteUrl,
|
||||
imageAlt: ogImageAltText,
|
||||
imageWidth: ogImageWidth ? String(ogImageWidth) : undefined,
|
||||
imageHeight: ogImageHeight ? String(ogImageHeight) : undefined,
|
||||
};
|
||||
|
||||
const twitter: PostTwitterMeta = {
|
||||
title: pageTitle,
|
||||
description: description,
|
||||
card: 'summary_large_image',
|
||||
site: siteOwnerTwitterHandle,
|
||||
creator: contentAuthorTwitterHandle || siteOwnerTwitterHandle,
|
||||
image: ogImageAbsoluteUrl,
|
||||
imageAlt: ogImageAltText,
|
||||
};
|
||||
const ogImageAbsoluteUrl = parseOgImageUrl(ogImageUrl);
|
||||
|
||||
return {
|
||||
og,
|
||||
twitter,
|
||||
og: {
|
||||
title: title,
|
||||
description: description,
|
||||
type: 'article',
|
||||
url: pageUrl,
|
||||
author: authorName,
|
||||
publishDate: publishDate,
|
||||
image: ogImageAbsoluteUrl,
|
||||
imageAlt: ogImageAltText,
|
||||
imageWidth: ogImageWidth ? String(ogImageWidth) : undefined,
|
||||
imageHeight: ogImageHeight ? String(ogImageHeight) : undefined,
|
||||
},
|
||||
twitter: {
|
||||
title: title,
|
||||
description: description,
|
||||
card: 'summary_large_image',
|
||||
site: siteOwnerTwitterHandle,
|
||||
creator: contentAuthorTwitterHandle || siteOwnerTwitterHandle,
|
||||
image: ogImageAbsoluteUrl,
|
||||
imageAlt: ogImageAltText,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
@ -7,10 +7,9 @@ import '@/assets/styles/reset.css';
|
||||
import '@/assets/styles/globals.css';
|
||||
|
||||
import Footer from '@/components/footer/Footer.astro';
|
||||
import GoTop from '@/components/footer/GoTop.astro';
|
||||
import Header from '@/components/header/Header.astro';
|
||||
import PageMeta from '@/components/meta/PageMeta.astro';
|
||||
import { options } from '@/helpers/schema';
|
||||
import options from '@/options';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
@ -54,7 +53,15 @@ const description = Astro.props.description || options.description;
|
||||
<slot />
|
||||
<Footer />
|
||||
</main>
|
||||
<GoTop />
|
||||
<ul class="site-fixed-widget">
|
||||
<li class="fixed-gotop">
|
||||
<div class="btn btn-light btn-icon btn-lg btn-rounded btn-gotop">
|
||||
<span>
|
||||
<i class="iconfont icon-arrowup"></i>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<script>
|
||||
import '../assets/scripts/yufan.me.js';
|
||||
|
@ -4,9 +4,10 @@ import Image from '@/components/image/Image.astro';
|
||||
import PageMeta from '@/components/meta/PageMeta.astro';
|
||||
import FriendLinks from '@/components/page/friend/FriendLinks.astro';
|
||||
import MusicPlayer from '@/components/player/MusicPlayer.astro';
|
||||
import { options, type Page } from '@/helpers/schema';
|
||||
import type { Page } from '@/helpers/schema';
|
||||
import { urlJoin } from '@/helpers/tools';
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import options from '@/options';
|
||||
|
||||
interface Props {
|
||||
page: Page;
|
||||
@ -17,7 +18,7 @@ const { Content } = await page.render();
|
||||
---
|
||||
|
||||
<BaseLayout title={page.title}>
|
||||
<PageMeta slot="og" title={page.title} ogImageUrl={urlJoin(import.meta.env.SITE, `/og/${page.slug}.png`)} />
|
||||
<PageMeta slot="og" title={page.title} ogImageUrl={`/og/${page.slug}.png`} />
|
||||
|
||||
<div class="px-lg-2 px-xxl-5 py-3 py-md-4 py-xxl-5">
|
||||
<div class="container">
|
||||
|
@ -7,9 +7,10 @@ import PostMeta from '@/components/meta/PostMeta.astro';
|
||||
import MusicPlayer from '@/components/player/MusicPlayer.astro';
|
||||
import Sidebar from '@/components/sidebar/Sidebar.astro';
|
||||
import { formatShowDate } from '@/helpers/formatter';
|
||||
import { options, posts, tags, type Post } from '@/helpers/schema';
|
||||
import { posts, tags, type Post } from '@/helpers/schema';
|
||||
import { urlJoin } from '@/helpers/tools';
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import options from '@/options';
|
||||
|
||||
interface Props {
|
||||
post: Post;
|
||||
@ -26,8 +27,8 @@ const { Content } = await post.render();
|
||||
description={post.summary}
|
||||
publishDate={post.date.toISOString()}
|
||||
requestPath={post.permalink}
|
||||
ogImageUrl={urlJoin(import.meta.env.SITE, `/og/${post.slug}.png`)}
|
||||
ogImageAltText={post.title}
|
||||
ogImageUrl={`/og/${post.slug}.png`}
|
||||
ogImageAltText={`${post.title} - ${post.summary}`}
|
||||
/>
|
||||
<div class="px-lg-2 px-xxl-5 py-3 py-md-4 py-xxl-5">
|
||||
<div class="container">
|
||||
|
@ -2,8 +2,9 @@
|
||||
import Pagination from '@/components/page/pagination/Pagination.astro';
|
||||
import PostSquare from '@/components/page/post/PostSquare.astro';
|
||||
import { slicePosts } from '@/helpers/formatter';
|
||||
import { options, type Category, type Post } from '@/helpers/schema';
|
||||
import type { Category, Post } from '@/helpers/schema';
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import options from '@/options';
|
||||
|
||||
interface Props {
|
||||
category: Category;
|
||||
|
@ -21,7 +21,6 @@ const { title, posts } = Astro.props;
|
||||
posts.length === 0 ? (
|
||||
<div class="data-null">
|
||||
<div class="my-auto">
|
||||
<i class="svg-404" />
|
||||
<h1 class="font-number">404</h1>
|
||||
<div>抱歉,没有你要找的内容...</div>
|
||||
</div>
|
||||
|
@ -2,8 +2,9 @@
|
||||
import Pagination from '@/components/page/pagination/Pagination.astro';
|
||||
import PostSquare from '@/components/page/post/PostSquare.astro';
|
||||
import { slicePosts } from '@/helpers/formatter';
|
||||
import { options, type Post, type Tag } from '@/helpers/schema';
|
||||
import type { Post, Tag } from '@/helpers/schema';
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import options from '@/options';
|
||||
|
||||
interface Props {
|
||||
tag: Tag;
|
||||
|
@ -5,7 +5,6 @@ import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
<BaseLayout title="未找到页面">
|
||||
<div class="data-null">
|
||||
<div class="my-auto">
|
||||
<i class="svg-404"></i>
|
||||
<h1 class="font-number">404</h1>
|
||||
<div>抱歉,没有你要找的内容...</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,7 @@
|
||||
---
|
||||
import { getCategory, options, posts } from '@/helpers/schema';
|
||||
import { getCategory, posts } from '@/helpers/schema';
|
||||
import CategoryLayout from '@/layouts/posts/CategoryLayout.astro';
|
||||
import options from '@/options';
|
||||
|
||||
const { slug, num } = Astro.params;
|
||||
const category = getCategory(undefined, slug);
|
||||
|
@ -1,7 +1,8 @@
|
||||
import PostContent from '@/components/page/post/PostContent.astro';
|
||||
import { partialRender } from '@/helpers/container';
|
||||
import { options, posts, type Post } from '@/helpers/schema';
|
||||
import { posts, type Post } from '@/helpers/schema';
|
||||
import { urlJoin } from '@/helpers/tools';
|
||||
import options from '@/options';
|
||||
import rss from '@astrojs/rss';
|
||||
import { ELEMENT_NODE, TEXT_NODE, transform, walk, type TextNode } from 'ultrahtml';
|
||||
import sanitize from 'ultrahtml/transformers/sanitize';
|
||||
@ -13,7 +14,7 @@ const cleanupContent = async (html: string) => {
|
||||
if (node.type === ELEMENT_NODE) {
|
||||
// Make sure images are absolute, some readers are not smart enough to figure it out
|
||||
if (node.name === 'img' && node.attributes.src?.startsWith('/')) {
|
||||
node.attributes.src = urlJoin(import.meta.env.SITE, node.attributes.src);
|
||||
node.attributes.src = urlJoin(options.assetsPrefix(), node.attributes.src);
|
||||
const { src, alt } = node.attributes;
|
||||
node.attributes = { src, alt };
|
||||
}
|
||||
|
@ -45,9 +45,9 @@ export const GET: APIRoute = async ({ params }) => {
|
||||
});
|
||||
};
|
||||
|
||||
export async function getStaticPaths() {
|
||||
export const getStaticPaths = async () => {
|
||||
return [
|
||||
...posts.map((post) => ({ params: { slug: post.slug } })),
|
||||
...pages.map((page) => ({ params: { slug: page.slug } })),
|
||||
];
|
||||
}
|
||||
};
|
||||
|
@ -1,7 +1,8 @@
|
||||
---
|
||||
import { options, posts } from '@/helpers/schema';
|
||||
import { posts } from '@/helpers/schema';
|
||||
import { searchPosts } from '@/helpers/search';
|
||||
import SearchLayout from '@/layouts/posts/SearchLayout.astro';
|
||||
import options from '@/options';
|
||||
|
||||
const query = Astro.url.searchParams.get('q') || '';
|
||||
if (query === '') {
|
||||
|
@ -3,7 +3,7 @@ import { urlJoin } from '@/helpers/tools';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export async function GET() {
|
||||
export const GET = async () => {
|
||||
const result = `
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
@ -26,4 +26,4 @@ export async function GET() {
|
||||
'Content-Type': 'application/xml',
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -1,6 +1,7 @@
|
||||
---
|
||||
import { getTag, options, posts } from '@/helpers/schema';
|
||||
import { getTag, posts } from '@/helpers/schema';
|
||||
import TagLayout from '@/layouts/posts/TagLayout.astro';
|
||||
import options from '@/options';
|
||||
|
||||
const { slug, num } = Astro.params;
|
||||
const tag = getTag(undefined, slug);
|
||||
|
@ -8,8 +8,8 @@
|
||||
"module": "ESNext",
|
||||
"strictNullChecks": true,
|
||||
"paths": {
|
||||
"@/options": ["./options.ts"],
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"types": ["vite-plugin-arraybuffer/types"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user