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:
Yufan Sheng 2024-06-19 03:34:36 +08:00
parent 7fec4ba787
commit 0126b71722
Signed by: syhily
GPG Key ID: 9D18A22A7DCD5A9B
49 changed files with 782 additions and 1018 deletions

27
.vscode/settings.json vendored
View File

@ -12,6 +12,10 @@
}, },
"cSpell.words": [ "cSpell.words": [
"alexinea", "alexinea",
"alignfull",
"alignleft",
"alignright",
"alignwide",
"ameho", "ameho",
"amehochan", "amehochan",
"aplayer", "aplayer",
@ -21,6 +25,7 @@
"batang", "batang",
"bigserial", "bigserial",
"biomejs", "biomejs",
"blogroll",
"blogster", "blogster",
"blurhash", "blurhash",
"captainofphb", "captainofphb",
@ -45,12 +50,13 @@
"fong", "fong",
"forencrypt", "forencrypt",
"giscus", "giscus",
"gogogo",
"gotop", "gotop",
"gungseo", "gungseo",
"hefei", "hefei",
"heiti", "heiti",
"HONORBKK", "honorbkk",
"HUAWEIYAL", "huaweiyal",
"ianvs", "ianvs",
"iconfont", "iconfont",
"iroha", "iroha",
@ -61,7 +67,7 @@
"jing", "jing",
"jungshik", "jungshik",
"khalil", "khalil",
"KHTML", "khtml",
"koanughi", "koanughi",
"koaunghi", "koaunghi",
"lantinghei", "lantinghei",
@ -73,15 +79,17 @@
"luxon", "luxon",
"mboker", "mboker",
"minagi", "minagi",
"Miui", "miui",
"MMWEBID", "mmwebid",
"MMWEBSDK", "mmwebsdk",
"mochi", "mochi",
"napi", "napi",
"netease", "netease",
"nextval", "nextval",
"nian", "nian",
"nocolor",
"nofollow", "nofollow",
"nopd",
"noto", "noto",
"oppo", "oppo",
"opposans", "opposans",
@ -95,7 +103,7 @@
"qrcode", "qrcode",
"quan", "quan",
"recma", "recma",
"Redmi", "redmi",
"regclass", "regclass",
"sauvignon", "sauvignon",
"sheng", "sheng",
@ -104,6 +112,8 @@
"shmily", "shmily",
"skrs", "skrs",
"syhily", "syhily",
"tabindex",
"tagcloud",
"taza", "taza",
"teruteru", "teruteru",
"timestamptz", "timestamptz",
@ -111,6 +121,7 @@
"tsconfigs", "tsconfigs",
"ultrahtml", "ultrahtml",
"unpic", "unpic",
"upyun",
"urlset", "urlset",
"varchar", "varchar",
"velite", "velite",
@ -120,7 +131,7 @@
"weibo", "weibo",
"xiao", "xiao",
"xinsenz", "xinsenz",
"XWEB", "xweb",
"yefengs", "yefengs",
"yetgul", "yetgul",
"ying", "ying",

View File

@ -8,7 +8,7 @@ COPY . .
ENV ASTRO_TELEMETRY_DISABLED=1 ENV ASTRO_TELEMETRY_DISABLED=1
RUN NODE_ENV=development npm install RUN NODE_ENV=development npm install
RUN npm i patch-package && npm exec patch-package RUN npm i patch-package && npm exec patch-package
RUN npm run build RUN NODE_ENV=production npm run build
FROM base AS runtime FROM base AS runtime
RUN npm install --omit=dev RUN npm install --omit=dev
@ -16,4 +16,4 @@ COPY --from=build /app/dist ./dist
ENV HOST=0.0.0.0 ENV HOST=0.0.0.0
ENV PORT=4321 ENV PORT=4321
EXPOSE 4321 EXPOSE 4321
CMD node ./dist/server/entry.mjs CMD NODE_ENV=production node ./dist/server/entry.mjs

View File

@ -1,15 +1,14 @@
import mdx from '@astrojs/mdx'; import mdx from '@astrojs/mdx';
import node from '@astrojs/node'; import node from '@astrojs/node';
import { defineConfig, envField } from 'astro/config'; import { defineConfig, envField } from 'astro/config';
import { astroImage } from './remark-plugins/images'; import options from './options';
import { astroImage } from './plugins/images';
// Dynamic switch the site. This is hard coded. import { upyun } from './plugins/upyun';
const port = 4321;
const site = import.meta.env.PROD ? 'https://yufan.me' : `http://localhost:${port}`;
// https://astro.build/config // https://astro.build/config
export default defineConfig({ 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', output: 'server',
security: { security: {
checkOrigin: true, checkOrigin: true,
@ -17,11 +16,13 @@ export default defineConfig({
experimental: { experimental: {
env: { env: {
schema: { schema: {
// Postgres Database
POSTGRES_HOST: envField.string({ context: 'server', access: 'secret' }), POSTGRES_HOST: envField.string({ context: 'server', access: 'secret' }),
POSTGRES_PORT: envField.number({ context: 'server', access: 'secret' }), POSTGRES_PORT: envField.number({ context: 'server', access: 'secret' }),
POSTGRES_USERNAME: envField.string({ context: 'server', access: 'secret' }), POSTGRES_USERNAME: envField.string({ context: 'server', access: 'secret' }),
POSTGRES_PASSWORD: envField.string({ context: 'server', access: 'secret' }), POSTGRES_PASSWORD: envField.string({ context: 'server', access: 'secret' }),
POSTGRES_DATABASE: envField.string({ context: 'server', access: 'secret' }), POSTGRES_DATABASE: envField.string({ context: 'server', access: 'secret' }),
// Artalk Comment
ARTALK_HOST: envField.string({ context: 'server', access: 'secret' }), ARTALK_HOST: envField.string({ context: 'server', access: 'secret' }),
}, },
}, },
@ -30,6 +31,9 @@ export default defineConfig({
mdx({ mdx({
remarkPlugins: [astroImage], remarkPlugins: [astroImage],
}), }),
upyun({
path: ['images', 'og', 'cats'],
}),
], ],
adapter: node({ adapter: node({
mode: 'standalone', mode: 'standalone',
@ -42,8 +46,7 @@ export default defineConfig({
}, },
}, },
server: { server: {
host: true, port: options.local.port,
port: port,
}, },
devToolbar: { devToolbar: {
// I don't need such toolbar. // I don't need such toolbar.
@ -53,4 +56,8 @@ export default defineConfig({
// Add this for avoiding the needless import optimize in Vite. // Add this for avoiding the needless import optimize in Vite.
optimizeDeps: { exclude: ['@napi-rs/canvas'] }, optimizeDeps: { exclude: ['@napi-rs/canvas'] },
}, },
build: {
assets: 'cats',
assetsPrefix: options.assetsPrefix(),
},
}); });

177
options.ts Normal file
View 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
View File

@ -32,6 +32,7 @@
"@types/pg": "^8.11.6", "@types/pg": "^8.11.6",
"@types/qrcode-svg": "^1.1.4", "@types/qrcode-svg": "^1.1.4",
"@types/unist": "^3.0.2", "@types/unist": "^3.0.2",
"@types/upyun": "^3.4.3",
"aplayer": "^1.10.1", "aplayer": "^1.10.1",
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"prettier": "^3.3.2", "prettier": "^3.3.2",
@ -42,7 +43,8 @@
"rimraf": "^5.0.7", "rimraf": "^5.0.7",
"sharp": "^0.33.4", "sharp": "^0.33.4",
"typescript": "^5.4.5", "typescript": "^5.4.5",
"unist-util-visit": "^5.0.0" "unist-util-visit": "^5.0.0",
"upyun": "^3.4.6"
} }
}, },
"node_modules/@ampproject/remapping": { "node_modules/@ampproject/remapping": {
@ -171,6 +173,29 @@
"integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==", "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==",
"license": "MIT" "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": { "node_modules/@astrojs/markdown-remark/node_modules/nlcst-to-string": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-3.1.1.tgz", "resolved": "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-3.1.1.tgz",
@ -2774,6 +2799,16 @@
"integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==", "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==",
"license": "MIT" "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": { "node_modules/@ungap/structured-clone": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
@ -3136,6 +3171,23 @@
"sharp": "^0.33.3" "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": { "node_modules/axobject-query": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz",
@ -3414,6 +3466,16 @@
"url": "https://github.com/sponsors/wooorm" "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": { "node_modules/chokidar": {
"version": "3.6.0", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@ -3685,6 +3747,19 @@
"devOptional": true, "devOptional": true,
"license": "MIT" "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": { "node_modules/comma-separated-tokens": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
@ -3730,6 +3805,16 @@
"node": ">= 8" "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": { "node_modules/cssesc": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@ -3772,6 +3857,16 @@
"url": "https://github.com/sponsors/wooorm" "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": { "node_modules/depd": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@ -4349,6 +4444,27 @@
"node": ">=8" "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": { "node_modules/foreground-child": {
"version": "3.2.1", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz",
@ -4366,6 +4482,21 @@
"url": "https://github.com/sponsors/isaacs" "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": { "node_modules/fresh": {
"version": "0.5.2", "version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
@ -4812,6 +4943,13 @@
"url": "https://opencollective.com/unified" "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": { "node_modules/html-escaper": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
@ -4925,27 +5063,11 @@
} }
}, },
"node_modules/is-buffer": { "node_modules/is-buffer": {
"version": "2.0.5", "version": "1.1.6",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
"integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
"funding": [ "dev": true,
{ "license": "MIT"
"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/is-core-module": { "node_modules/is-core-module": {
"version": "2.13.1", "version": "2.13.1",
@ -5084,6 +5206,13 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/is-reference": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz",
@ -5393,6 +5522,18 @@
"node": ">= 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": { "node_modules/mdast-util-definitions": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz", "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz",
@ -6464,6 +6605,29 @@
"node": ">=4" "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": { "node_modules/mimic-fn": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
@ -8609,6 +8773,25 @@
"browserslist": ">= 4.21.0" "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": { "node_modules/vfile": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz",

View File

@ -64,6 +64,7 @@
"@types/pg": "^8.11.6", "@types/pg": "^8.11.6",
"@types/qrcode-svg": "^1.1.4", "@types/qrcode-svg": "^1.1.4",
"@types/unist": "^3.0.2", "@types/unist": "^3.0.2",
"@types/upyun": "^3.4.3",
"aplayer": "^1.10.1", "aplayer": "^1.10.1",
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"prettier": "^3.3.2", "prettier": "^3.3.2",
@ -74,6 +75,7 @@
"rimraf": "^5.0.7", "rimraf": "^5.0.7",
"sharp": "^0.33.4", "sharp": "^0.33.4",
"typescript": "^5.4.5", "typescript": "^5.4.5",
"unist-util-visit": "^5.0.0" "unist-util-visit": "^5.0.0",
"upyun": "^3.4.6"
} }
} }

View File

@ -43,7 +43,7 @@ const transformAstroImage = async (imageNode: ImageNode) => {
imageNode.name = 'Image'; imageNode.name = 'Image';
imageNode.attributes = [ imageNode.attributes = [
{ type: 'mdxJsxAttribute', name: 'alt', value: imageNode.alt }, { 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: 'width', value: imageNode.width ?? metadata.width },
{ type: 'mdxJsxAttribute', name: 'height', value: imageNode.height ?? metadata.height }, { type: 'mdxJsxAttribute', name: 'height', value: imageNode.height ?? metadata.height },
{ type: 'mdxJsxAttribute', name: 'blurDataURL', value: metadata.blurDataURL }, { type: 'mdxJsxAttribute', name: 'blurDataURL', value: metadata.blurDataURL },

89
plugins/upyun.ts Normal file
View 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

View File

@ -1,3 +1,6 @@
/*--------------------------------------------------------------
color variables
--------------------------------------------------------------*/
:root { :root {
--color-primary: #008c95; --color-primary: #008c95;
--color-dark: #151b2b; --color-dark: #151b2b;
@ -76,7 +79,6 @@
} }
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* 0. CSS Reset /* 0. CSS Reset
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
@ -190,7 +192,6 @@ table caption {
} }
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* 1. Document Setup /* 1. Document Setup
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */

View File

@ -44,7 +44,7 @@ const { comment, config, depth, pending } = Astro.props;
</div> </div>
<div class="comment-footer text-xs text-muted"> <div class="comment-footer text-xs text-muted">
<time class="me-2">{formatLocalDate(comment.date)}</time> <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>
</div> </div>
</article> </article>

View File

@ -8,8 +8,8 @@ import type {
ErrorResp, ErrorResp,
} from '@/components/comment/types'; } from '@/components/comment/types';
import { increaseViews } from '@/helpers/db/query'; import { increaseViews } from '@/helpers/db/query';
import { options } from '@/helpers/schema';
import { urlJoin } from '@/helpers/tools'; import { urlJoin } from '@/helpers/tools';
import options from '@/options';
import { ARTALK_HOST } from 'astro:env/server'; import { ARTALK_HOST } from 'astro:env/server';
import _ from 'lodash'; import _ from 'lodash';
import { marked } from 'marked'; import { marked } from 'marked';
@ -18,7 +18,7 @@ import { ELEMENT_NODE, transform, walk } from 'ultrahtml';
import sanitize from 'ultrahtml/transformers/sanitize'; import sanitize from 'ultrahtml/transformers/sanitize';
// Access the artalk in internal docker host when it was deployed on zeabur. // 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> => { export const getConfig = async (): Promise<CommentConfig | null> => {
const data = await fetch(urlJoin(server, '/api/v2/conf')) const data = await fetch(urlJoin(server, '/api/v2/conf'))

View File

@ -1,24 +1,42 @@
--- ---
import options from '@/options';
import { DateTime } from 'luxon'; 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"> <footer class="footer border-top border-light text-xs text-center py-4 py-xl-5">
Copyright © {options.settings.initialYear}-{currentYear()}{' '} <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"> <a href={import.meta.env.SITE} title={options.title} rel="home">
{options.title} {options.title}
</a> </a>
<br /> </div>
{ {
options.settings.icpNo && ( options.settings.icpNo && (
<a href="https://beian.miit.gov.cn" rel="nofollow" target="_blank" title={'良民证'}> <div class="line">
<a href="https://beian.miit.gov.cn" rel="nofollow" target="_blank" title="ICP 备案">
{options.settings.icpNo} {options.settings.icpNo}
</a> </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> </footer>

View File

@ -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>

View File

@ -3,7 +3,7 @@ import Logo from '@/components/header/Logo.astro';
import LogoLarge from '@/components/header/LogoLarge.astro'; import LogoLarge from '@/components/header/LogoLarge.astro';
import QRDialog from '@/components/image/QRDialog.astro'; import QRDialog from '@/components/image/QRDialog.astro';
import SearchDialog from '@/components/search/SearchDialog.astro'; import SearchDialog from '@/components/search/SearchDialog.astro';
import { options } from '@/helpers/schema'; import options from '@/options';
--- ---
<header class="site-aside"> <header class="site-aside">

View File

@ -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> <title>{options.title}</title>
<g id="logo-dark" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="logo-dark" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(9, 16)"> <g transform="translate(9, 16)">

View File

@ -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> <title>{options.title}</title>
<g id="logo-large" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="logo-large" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="logo-white" transform="translate(20, 15)"> <g id="logo-white" transform="translate(20, 15)">

View File

@ -1,7 +1,8 @@
--- ---
import QRDialog from '@/components/image/QRDialog.astro'; 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 { urlJoin } from '@/helpers/tools';
import options from '@/options';
import * as querystring from 'node:querystring'; import * as querystring from 'node:querystring';
interface Props { interface Props {

View File

@ -1,8 +1,7 @@
--- ---
import { openGraphHeight, openGraphWidth } from '@/helpers/images'; import { openGraphHeight, openGraphWidth } from '@/helpers/images';
import { options } from '@/helpers/schema';
import { getPageMeta } from '@/helpers/seo'; import { getPageMeta } from '@/helpers/seo';
import { urlJoin } from '@/helpers/tools'; import options from '@/options';
export interface Props { export interface Props {
title?: string; title?: string;
@ -16,12 +15,7 @@ const { og, twitter } = getPageMeta({
title: title || options.title, title: title || options.title,
description: description || options.description, description: description || options.description,
baseUrl: import.meta.env.SITE, baseUrl: import.meta.env.SITE,
ogImageAbsoluteUrl: ogImageUrl: ogImageUrl,
ogImageUrl === undefined
? urlJoin(import.meta.env.SITE, '/images/open-graph.png')
: ogImageUrl.startsWith('/')
? urlJoin(import.meta.env.SITE, ogImageUrl)
: ogImageUrl,
ogImageAltText: title || options.title, ogImageAltText: title || options.title,
ogImageWidth: openGraphWidth, ogImageWidth: openGraphWidth,
ogImageHeight: openGraphHeight, ogImageHeight: openGraphHeight,

View File

@ -1,8 +1,7 @@
--- ---
import { openGraphHeight, openGraphWidth } from '@/helpers/images'; import { openGraphHeight, openGraphWidth } from '@/helpers/images';
import { options } from '@/helpers/schema';
import { getBlogPostMeta } from '@/helpers/seo'; import { getBlogPostMeta } from '@/helpers/seo';
import { urlJoin } from '@/helpers/tools'; import options from '@/options';
export interface Props { export interface Props {
title?: string; title?: string;
@ -10,7 +9,7 @@ export interface Props {
publishDate: string; publishDate: string;
requestPath: string; requestPath: string;
ogImageUrl?: string; ogImageUrl?: string;
ogImageAltText?: string; ogImageAltText: string;
} }
const { requestPath, title, description, publishDate, ogImageUrl, ogImageAltText } = Astro.props; 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, pageUrl: requestPath.startsWith('http') ? requestPath : import.meta.env.SITE + requestPath,
authorName: options.author.name, authorName: options.author.name,
publishDate, publishDate,
ogImageAbsoluteUrl: ogImageUrl: ogImageUrl,
typeof ogImageUrl === 'undefined'
? urlJoin(import.meta.env.SITE, '/images/open-graph.png')
: ogImageUrl.startsWith('/')
? urlJoin(import.meta.env.SITE, ogImageUrl)
: ogImageUrl,
ogImageAltText, ogImageAltText,
ogImageWidth: openGraphWidth, ogImageWidth: openGraphWidth,
ogImageHeight: openGraphHeight, ogImageHeight: openGraphHeight,

View File

@ -1,6 +1,7 @@
--- ---
import PinnedCategory from '@/components/page/category/PinnedCategory.astro'; 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 pinnedSlug = options.settings.post.category ?? [];
const pinnedCategories = pinnedSlug const pinnedCategories = pinnedSlug

View File

@ -1,6 +1,7 @@
--- ---
import FeaturePost from '@/components/page/post/FeaturePost.astro'; 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 { interface Props {
posts: Post[]; posts: Post[];

View File

@ -2,7 +2,8 @@
import Pagination from '@/components/page/pagination/Pagination.astro'; import Pagination from '@/components/page/pagination/Pagination.astro';
import PostCards from '@/components/page/post/PostCards.astro'; import PostCards from '@/components/page/post/PostCards.astro';
import { slicePosts } from '@/helpers/formatter'; import { slicePosts } from '@/helpers/formatter';
import { options, type Post } from '@/helpers/schema'; import type { Post } from '@/helpers/schema';
import options from '@/options';
interface Props { interface Props {
posts: Post[]; posts: Post[];

View File

@ -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}> <div id="search" class="widget widget_search" hidden={!options.settings.sidebar.search}>

View File

@ -1,7 +1,8 @@
--- ---
import _ from 'lodash'; import _ from 'lodash';
import { options, type Post } from '@/helpers/schema'; import type { Post } from '@/helpers/schema';
import options from '@/options';
interface Props { interface Props {
posts: Post[]; posts: Post[];

View File

@ -1,7 +1,8 @@
--- ---
import _ from 'lodash'; import _ from 'lodash';
import { options, type Tag } from '@/helpers/schema'; import type { Tag } from '@/helpers/schema';
import options from '@/options';
interface Props { interface Props {
tags: Tag[]; tags: Tag[];

View File

@ -1,6 +1,6 @@
--- ---
import { latestComments } from '@/helpers/db/query'; import { latestComments } from '@/helpers/db/query';
import { options } from '@/helpers/schema'; import options from '@/options';
const comments = await latestComments(); const comments = await latestComments();
--- ---

View File

@ -1,4 +1,6 @@
import { imageMetadata } from '@/helpers/images'; import { imageMetadata } from '@/helpers/images';
import { urlJoin } from '@/helpers/tools';
import options from '@/options';
import { defineCollection, z } from 'astro:content'; import { defineCollection, z } from 'astro:content';
export const defaultCover = '/images/default-cover.jpg'; export const defaultCover = '/images/default-cover.jpg';
@ -40,7 +42,9 @@ const friendsCollection = defineCollection({
website: z.string().max(40), website: z.string().max(40),
description: z.string().optional(), description: z.string().optional(),
homepage: z.string().url(), homepage: z.string().url(),
poster: z.string(), poster: z
.string()
.transform((poster) => (poster.startsWith('/') ? urlJoin(options.assetsPrefix(), poster) : poster)),
favicon: z.string().optional(), favicon: z.string().optional(),
}) })
.transform((data) => { .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 // Posts Collection
const postsCollection = defineCollection({ const postsCollection = defineCollection({
type: 'content', type: 'content',
@ -151,7 +100,6 @@ const tagsCollection = defineCollection({
export const collections = { export const collections = {
categories: categoriesCollection, categories: categoriesCollection,
friends: friendsCollection, friends: friendsCollection,
options: optionsCollection,
pages: pagesCollection, pages: pagesCollection,
posts: postsCollection, posts: postsCollection,
tags: tagsCollection, tags: tagsCollection,

View File

@ -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

View File

@ -1,7 +1,7 @@
import { db } from '@/helpers/db/pool'; import { db } from '@/helpers/db/pool';
import { atk_comments, atk_likes, atk_pages, atk_users } from '@/helpers/db/schema'; 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 { makeToken, urlJoin } from '@/helpers/tools';
import options from '@/options';
import { and, desc, eq, isNull, notInArray, sql } from 'drizzle-orm'; import { and, desc, eq, isNull, notInArray, sql } from 'drizzle-orm';
export interface Comment { export interface Comment {

View File

@ -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'; import { DateTime } from 'luxon';
export const slicePosts = ( export const slicePosts = (

View File

@ -1,5 +1,7 @@
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
import { join } from 'node:path'; import { join } from 'node:path';
import options from '../../options';
import { urlJoin } from './tools';
export interface Image { export interface Image {
/** /**
@ -55,7 +57,7 @@ export const imageMetadata = async (publicPath: string): Promise<Image> => {
const { default: sharp } = await import('sharp'); const { default: sharp } = await import('sharp');
if (!publicPath.startsWith('/')) { 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'); 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 blurImage = await img.resize(blurWidth, blurHeight).webp({ quality: 1 }).toBuffer();
const blurDataURL = `data:image/webp;base64,${blurImage.toString('base64')}`; 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 };
}; };

View File

@ -5,7 +5,7 @@
* But I have get the approvement to use them here by asking the author https://twitter.com/yuaanlin. * 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 { 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 { Canvas, GlobalFonts, Image, type SKRSContext2D } from '@napi-rs/canvas';
import { readFile } from 'node:fs/promises'; import { readFile } from 'node:fs/promises';
import { join } from 'node:path'; import { join } from 'node:path';
@ -117,6 +117,11 @@ const drawImageProp = (
}; };
const fetchCover = async (cover: string): Promise<Buffer> => { 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')) { if (cover.startsWith('http')) {
return Buffer.from(await (await fetch(cover)).arrayBuffer()); return Buffer.from(await (await fetch(cover)).arrayBuffer());
} }
@ -132,7 +137,7 @@ export interface OpenGraphProps {
} }
export const defaultOpenGraph = async (): Promise<Buffer> => { 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 // Register the font if it doesn't exist

View File

@ -1,10 +1,10 @@
import { defaultCover } from '@/content/config.ts'; import { defaultCover } from '@/content/config.ts';
import options from '@/options';
import { getCollection, getEntryBySlug, type Render } from 'astro:content'; import { getCollection, getEntryBySlug, type Render } from 'astro:content';
// Import the collections from the astro content. // Import the collections from the astro content.
const categoriesCollection = await getCollection('categories'); const categoriesCollection = await getCollection('categories');
const friendsCollection = await getCollection('friends'); const friendsCollection = await getCollection('friends');
const optionsCollection = await getCollection('options');
const pagesCollection = await getCollection('pages'); const pagesCollection = await getCollection('pages');
const postsCollection = await getCollection('posts'); const postsCollection = await getCollection('posts');
const tagsCollection = await getCollection('tags'); const tagsCollection = await getCollection('tags');
@ -12,7 +12,6 @@ const tagsCollection = await getCollection('tags');
// Redefine the types from the astro content. // Redefine the types from the astro content.
export type Category = (typeof categoriesCollection)[number]['data'] & { counts: number; permalink: string }; export type Category = (typeof categoriesCollection)[number]['data'] & { counts: number; permalink: string };
export type Friend = (typeof friendsCollection)[number]['data'][number]; export type Friend = (typeof friendsCollection)[number]['data'][number];
export type Options = (typeof optionsCollection)[number]['data'];
export type Page = (typeof pagesCollection)[number]['data'] & { export type Page = (typeof pagesCollection)[number]['data'] & {
slug: string; slug: string;
permalink: 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. // 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[0].data;
export const options: Options = optionsCollection[0].data;
// Override the website for local debugging // Override the website for local debugging
export const pages: Page[] = pagesCollection export const pages: Page[] = pagesCollection
.filter((page) => page.data.published || !import.meta.env.PROD) .filter((page) => page.data.published || !options.isProd())
.map((page) => ({ .map((page) => ({
slug: page.slug, slug: page.slug,
permalink: `/${page.slug}`, permalink: `/${page.slug}`,
@ -42,7 +40,7 @@ export const pages: Page[] = pagesCollection
...page.data, ...page.data,
})); }));
export const posts: Post[] = postsCollection export const posts: Post[] = postsCollection
.filter((post) => post.data.published || !import.meta.env.PROD) .filter((post) => post.data.published || !options.isProd())
.map((post) => ({ .map((post) => ({
slug: post.slug, slug: post.slug,
permalink: `/posts/${post.slug}`, permalink: `/posts/${post.slug}`,
@ -90,7 +88,7 @@ if (missingTags.length > 0) {
const missingCovers = posts const missingCovers = posts
.filter((post) => post.cover.src === defaultCover) .filter((post) => post.cover.src === defaultCover)
.map((post) => ({ title: post.title, slug: post.slug })); .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. // We only warn here for this is a known improvement.
console.warn(`The following ${missingCovers.length} posts don't have a cover.`); console.warn(`The following ${missingCovers.length} posts don't have a cover.`);
console.warn(missingCovers); console.warn(missingCovers);
@ -124,13 +122,6 @@ if (invalidPinnedCategories.length > 0) {
throw new Error(`The bellowing pinned categories are invalid:\n$${invalidPinnedCategories.join('\n')}`); 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 => { export const getPost = (slug: string): Post | undefined => {
return posts.find((post) => post.slug === slug); return posts.find((post) => post.slug === slug);
}; };

View File

@ -1,7 +1,45 @@
// This file is copied from https://github.com/flexdinesh/blogster/blob/main/packages/shared/src/seo.ts // 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. // 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; title: string;
description?: string; description?: string;
type: 'website'; type: 'website';
@ -10,19 +48,9 @@ type PageOgMeta = {
imageAlt?: string; imageAlt?: string;
imageWidth?: string; imageWidth?: string;
imageHeight?: string; imageHeight?: string;
}; }
type PageTwitterMeta = { export interface PostOgMeta {
title: string;
description?: string;
card: 'summary_large_image';
site?: string;
creator?: string;
image?: string;
imageAlt?: string;
};
type PostOgMeta = {
title: string; title: string;
description?: string; description?: string;
type: 'article'; type: 'article';
@ -34,48 +62,34 @@ type PostOgMeta = {
imageAlt?: string; imageAlt?: string;
imageWidth?: string; imageWidth?: string;
imageHeight?: string; imageHeight?: string;
}; }
type PostTwitterMeta = { const parseOgImageUrl = (ogImageUrl?: string): string =>
title: string; typeof ogImageUrl === 'undefined'
description?: string; ? options.defaultOpenGraph()
card: 'summary_large_image'; : ogImageUrl.startsWith('/')
site?: string; ? urlJoin(options.assetsPrefix(), ogImageUrl)
creator?: string; : ogImageUrl;
image?: string;
imageAlt?: string;
};
export function getPageMeta({ export const getPageMeta = ({
title: pageTitle, title,
description, description,
baseUrl, baseUrl,
ogImageAbsoluteUrl, ogImageUrl,
ogImageAltText, ogImageAltText,
ogImageWidth, ogImageWidth,
ogImageHeight, ogImageHeight,
siteOwnerTwitterHandle, siteOwnerTwitterHandle,
contentAuthorTwitterHandle, contentAuthorTwitterHandle,
}: { }: PageMeta): { og: PageOgMeta; twitter: TwitterOgMeta } => {
title: string; if (!title) {
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) {
throw Error('title is required for page SEO'); throw Error('title is required for page SEO');
} }
if (ogImageAbsoluteUrl) { const ogImageAbsoluteUrl = parseOgImageUrl(ogImageUrl);
ogImageAltText = !ogImageAltText ? `Preview image for ${pageTitle}` : ogImageAltText;
}
const og: PageOgMeta = { return {
title: pageTitle, og: {
title: title,
description: description, description: description,
type: 'website', type: 'website',
url: baseUrl, url: baseUrl,
@ -83,58 +97,40 @@ export function getPageMeta({
imageAlt: ogImageAltText, imageAlt: ogImageAltText,
imageWidth: ogImageWidth ? String(ogImageWidth) : undefined, imageWidth: ogImageWidth ? String(ogImageWidth) : undefined,
imageHeight: ogImageHeight ? String(ogImageHeight) : undefined, imageHeight: ogImageHeight ? String(ogImageHeight) : undefined,
}; },
twitter: {
const twitter: PageTwitterMeta = { title: title,
title: pageTitle,
description: description, description: description,
card: 'summary_large_image', card: 'summary_large_image',
site: siteOwnerTwitterHandle, site: siteOwnerTwitterHandle,
creator: contentAuthorTwitterHandle || siteOwnerTwitterHandle, creator: contentAuthorTwitterHandle || siteOwnerTwitterHandle,
image: ogImageAbsoluteUrl, image: ogImageAbsoluteUrl,
imageAlt: ogImageAltText, imageAlt: ogImageAltText,
},
};
}; };
return { export const getBlogPostMeta = ({
og, title,
twitter,
};
}
export function getBlogPostMeta({
title: pageTitle,
description, description,
pageUrl, pageUrl,
authorName, authorName,
publishDate, publishDate,
ogImageAbsoluteUrl, ogImageUrl,
ogImageAltText, ogImageAltText,
ogImageWidth, ogImageWidth,
ogImageHeight, ogImageHeight,
siteOwnerTwitterHandle, siteOwnerTwitterHandle,
contentAuthorTwitterHandle, contentAuthorTwitterHandle,
}: { }: PostMeta): { og: PostOgMeta; twitter: TwitterOgMeta } => {
title: string; if (!title) {
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) {
throw Error('title is required for page SEO'); throw Error('title is required for page SEO');
} }
if (ogImageAbsoluteUrl && !ogImageAltText) { const ogImageAbsoluteUrl = parseOgImageUrl(ogImageUrl);
ogImageAltText = `Preview image for ${pageTitle}`;
}
const og: PostOgMeta = { return {
title: pageTitle, og: {
title: title,
description: description, description: description,
type: 'article', type: 'article',
url: pageUrl, url: pageUrl,
@ -144,20 +140,15 @@ export function getBlogPostMeta({
imageAlt: ogImageAltText, imageAlt: ogImageAltText,
imageWidth: ogImageWidth ? String(ogImageWidth) : undefined, imageWidth: ogImageWidth ? String(ogImageWidth) : undefined,
imageHeight: ogImageHeight ? String(ogImageHeight) : undefined, imageHeight: ogImageHeight ? String(ogImageHeight) : undefined,
}; },
twitter: {
const twitter: PostTwitterMeta = { title: title,
title: pageTitle,
description: description, description: description,
card: 'summary_large_image', card: 'summary_large_image',
site: siteOwnerTwitterHandle, site: siteOwnerTwitterHandle,
creator: contentAuthorTwitterHandle || siteOwnerTwitterHandle, creator: contentAuthorTwitterHandle || siteOwnerTwitterHandle,
image: ogImageAbsoluteUrl, image: ogImageAbsoluteUrl,
imageAlt: ogImageAltText, imageAlt: ogImageAltText,
},
}; };
return {
og,
twitter,
}; };
}

View File

@ -7,10 +7,9 @@ import '@/assets/styles/reset.css';
import '@/assets/styles/globals.css'; import '@/assets/styles/globals.css';
import Footer from '@/components/footer/Footer.astro'; import Footer from '@/components/footer/Footer.astro';
import GoTop from '@/components/footer/GoTop.astro';
import Header from '@/components/header/Header.astro'; import Header from '@/components/header/Header.astro';
import PageMeta from '@/components/meta/PageMeta.astro'; import PageMeta from '@/components/meta/PageMeta.astro';
import { options } from '@/helpers/schema'; import options from '@/options';
interface Props { interface Props {
title?: string; title?: string;
@ -54,7 +53,15 @@ const description = Astro.props.description || options.description;
<slot /> <slot />
<Footer /> <Footer />
</main> </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> </div>
<script> <script>
import '../assets/scripts/yufan.me.js'; import '../assets/scripts/yufan.me.js';

View File

@ -4,9 +4,10 @@ import Image from '@/components/image/Image.astro';
import PageMeta from '@/components/meta/PageMeta.astro'; import PageMeta from '@/components/meta/PageMeta.astro';
import FriendLinks from '@/components/page/friend/FriendLinks.astro'; import FriendLinks from '@/components/page/friend/FriendLinks.astro';
import MusicPlayer from '@/components/player/MusicPlayer.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 { urlJoin } from '@/helpers/tools';
import BaseLayout from '@/layouts/BaseLayout.astro'; import BaseLayout from '@/layouts/BaseLayout.astro';
import options from '@/options';
interface Props { interface Props {
page: Page; page: Page;
@ -17,7 +18,7 @@ const { Content } = await page.render();
--- ---
<BaseLayout title={page.title}> <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="px-lg-2 px-xxl-5 py-3 py-md-4 py-xxl-5">
<div class="container"> <div class="container">

View File

@ -7,9 +7,10 @@ import PostMeta from '@/components/meta/PostMeta.astro';
import MusicPlayer from '@/components/player/MusicPlayer.astro'; import MusicPlayer from '@/components/player/MusicPlayer.astro';
import Sidebar from '@/components/sidebar/Sidebar.astro'; import Sidebar from '@/components/sidebar/Sidebar.astro';
import { formatShowDate } from '@/helpers/formatter'; 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 { urlJoin } from '@/helpers/tools';
import BaseLayout from '@/layouts/BaseLayout.astro'; import BaseLayout from '@/layouts/BaseLayout.astro';
import options from '@/options';
interface Props { interface Props {
post: Post; post: Post;
@ -26,8 +27,8 @@ const { Content } = await post.render();
description={post.summary} description={post.summary}
publishDate={post.date.toISOString()} publishDate={post.date.toISOString()}
requestPath={post.permalink} requestPath={post.permalink}
ogImageUrl={urlJoin(import.meta.env.SITE, `/og/${post.slug}.png`)} ogImageUrl={`/og/${post.slug}.png`}
ogImageAltText={post.title} ogImageAltText={`${post.title} - ${post.summary}`}
/> />
<div class="px-lg-2 px-xxl-5 py-3 py-md-4 py-xxl-5"> <div class="px-lg-2 px-xxl-5 py-3 py-md-4 py-xxl-5">
<div class="container"> <div class="container">

View File

@ -2,8 +2,9 @@
import Pagination from '@/components/page/pagination/Pagination.astro'; import Pagination from '@/components/page/pagination/Pagination.astro';
import PostSquare from '@/components/page/post/PostSquare.astro'; import PostSquare from '@/components/page/post/PostSquare.astro';
import { slicePosts } from '@/helpers/formatter'; 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 BaseLayout from '@/layouts/BaseLayout.astro';
import options from '@/options';
interface Props { interface Props {
category: Category; category: Category;

View File

@ -21,7 +21,6 @@ const { title, posts } = Astro.props;
posts.length === 0 ? ( posts.length === 0 ? (
<div class="data-null"> <div class="data-null">
<div class="my-auto"> <div class="my-auto">
<i class="svg-404" />
<h1 class="font-number">404</h1> <h1 class="font-number">404</h1>
<div>抱歉,没有你要找的内容...</div> <div>抱歉,没有你要找的内容...</div>
</div> </div>

View File

@ -2,8 +2,9 @@
import Pagination from '@/components/page/pagination/Pagination.astro'; import Pagination from '@/components/page/pagination/Pagination.astro';
import PostSquare from '@/components/page/post/PostSquare.astro'; import PostSquare from '@/components/page/post/PostSquare.astro';
import { slicePosts } from '@/helpers/formatter'; 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 BaseLayout from '@/layouts/BaseLayout.astro';
import options from '@/options';
interface Props { interface Props {
tag: Tag; tag: Tag;

View File

@ -5,7 +5,6 @@ import BaseLayout from '@/layouts/BaseLayout.astro';
<BaseLayout title="未找到页面"> <BaseLayout title="未找到页面">
<div class="data-null"> <div class="data-null">
<div class="my-auto"> <div class="my-auto">
<i class="svg-404"></i>
<h1 class="font-number">404</h1> <h1 class="font-number">404</h1>
<div>抱歉,没有你要找的内容...</div> <div>抱歉,没有你要找的内容...</div>
</div> </div>

View File

@ -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 CategoryLayout from '@/layouts/posts/CategoryLayout.astro';
import options from '@/options';
const { slug, num } = Astro.params; const { slug, num } = Astro.params;
const category = getCategory(undefined, slug); const category = getCategory(undefined, slug);

View File

@ -1,7 +1,8 @@
import PostContent from '@/components/page/post/PostContent.astro'; import PostContent from '@/components/page/post/PostContent.astro';
import { partialRender } from '@/helpers/container'; 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 { urlJoin } from '@/helpers/tools';
import options from '@/options';
import rss from '@astrojs/rss'; import rss from '@astrojs/rss';
import { ELEMENT_NODE, TEXT_NODE, transform, walk, type TextNode } from 'ultrahtml'; import { ELEMENT_NODE, TEXT_NODE, transform, walk, type TextNode } from 'ultrahtml';
import sanitize from 'ultrahtml/transformers/sanitize'; import sanitize from 'ultrahtml/transformers/sanitize';
@ -13,7 +14,7 @@ const cleanupContent = async (html: string) => {
if (node.type === ELEMENT_NODE) { if (node.type === ELEMENT_NODE) {
// Make sure images are absolute, some readers are not smart enough to figure it out // Make sure images are absolute, some readers are not smart enough to figure it out
if (node.name === 'img' && node.attributes.src?.startsWith('/')) { 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; const { src, alt } = node.attributes;
node.attributes = { src, alt }; node.attributes = { src, alt };
} }

View File

@ -45,9 +45,9 @@ export const GET: APIRoute = async ({ params }) => {
}); });
}; };
export async function getStaticPaths() { export const getStaticPaths = async () => {
return [ return [
...posts.map((post) => ({ params: { slug: post.slug } })), ...posts.map((post) => ({ params: { slug: post.slug } })),
...pages.map((page) => ({ params: { slug: page.slug } })), ...pages.map((page) => ({ params: { slug: page.slug } })),
]; ];
} };

View File

@ -1,7 +1,8 @@
--- ---
import { options, posts } from '@/helpers/schema'; import { posts } from '@/helpers/schema';
import { searchPosts } from '@/helpers/search'; import { searchPosts } from '@/helpers/search';
import SearchLayout from '@/layouts/posts/SearchLayout.astro'; import SearchLayout from '@/layouts/posts/SearchLayout.astro';
import options from '@/options';
const query = Astro.url.searchParams.get('q') || ''; const query = Astro.url.searchParams.get('q') || '';
if (query === '') { if (query === '') {

View File

@ -3,7 +3,7 @@ import { urlJoin } from '@/helpers/tools';
export const prerender = true; export const prerender = true;
export async function GET() { export const GET = async () => {
const result = ` const result = `
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
@ -26,4 +26,4 @@ export async function GET() {
'Content-Type': 'application/xml', 'Content-Type': 'application/xml',
}, },
}); });
} };

View File

@ -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 TagLayout from '@/layouts/posts/TagLayout.astro';
import options from '@/options';
const { slug, num } = Astro.params; const { slug, num } = Astro.params;
const tag = getTag(undefined, slug); const tag = getTag(undefined, slug);

View File

@ -8,8 +8,8 @@
"module": "ESNext", "module": "ESNext",
"strictNullChecks": true, "strictNullChecks": true,
"paths": { "paths": {
"@/options": ["./options.ts"],
"@/*": ["src/*"] "@/*": ["src/*"]
}, }
"types": ["vite-plugin-arraybuffer/types"]
} }
} }