feat: init
12
.editorconfig
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# EditorConfig is awesome: https://EditorConfig.org
|
||||||
|
|
||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
insert_final_newline = false
|
||||||
23
.env.example
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
CHANNEL=Broadcast_Channel_Blog
|
||||||
|
|
||||||
|
LOCALE=zh-cn
|
||||||
|
TIMEZONE="Asia/Shanghai"
|
||||||
|
|
||||||
|
TELEGRAM=ccbikai
|
||||||
|
TWITTER=ccbikai
|
||||||
|
GITHUB=ccbikai
|
||||||
|
DISCORD=https://DISCORD.com
|
||||||
|
PODCASRT=https://PODCASRT.com
|
||||||
|
|
||||||
|
FOOTER_INJECT=FOOTER_INJECT
|
||||||
|
HEADER_INJECT=HEADER_INJECT
|
||||||
|
|
||||||
|
NO_FOLLOW=false
|
||||||
|
NO_INDEX=false
|
||||||
|
|
||||||
|
SENTRY_AUTH_TOKEN=SENTRY_AUTH_TOKEN
|
||||||
|
SENTRY_DSN=SENTRY_DSN
|
||||||
|
SENTRY_PROJECT=SENTRY_PROJECT
|
||||||
|
|
||||||
|
HOST="telegram.dog"
|
||||||
|
STATIC_PROXY=""
|
||||||
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# build output
|
||||||
|
dist/
|
||||||
|
# generated types
|
||||||
|
.astro/
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
|
||||||
|
# environment variables
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# macOS-specific files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# jetbrains setting folder
|
||||||
|
.idea/
|
||||||
1
.node-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
v20
|
||||||
4
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["astro-build.astro-vscode"],
|
||||||
|
"unwantedRecommendations": []
|
||||||
|
}
|
||||||
11
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"command": "./node_modules/.bin/astro dev",
|
||||||
|
"name": "Development server",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "node-terminal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
49
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
// Disable the default formatter, use eslint instead
|
||||||
|
"prettier.enable": false,
|
||||||
|
"editor.formatOnSave": false,
|
||||||
|
|
||||||
|
// Auto fix
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit",
|
||||||
|
"source.organizeImports": "never"
|
||||||
|
},
|
||||||
|
|
||||||
|
// Silent the stylistic rules in you IDE, but still auto fix them
|
||||||
|
"eslint.rules.customizations": [
|
||||||
|
{ "rule": "style/*", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "format/*", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*-indent", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*-spacing", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*-spaces", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*-order", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*-dangle", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*-newline", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*quotes", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*semi", "severity": "off", "fixable": true }
|
||||||
|
],
|
||||||
|
|
||||||
|
// Enable eslint for all supported languages
|
||||||
|
"eslint.validate": [
|
||||||
|
"javascript",
|
||||||
|
"javascriptreact",
|
||||||
|
"typescript",
|
||||||
|
"typescriptreact",
|
||||||
|
"vue",
|
||||||
|
"html",
|
||||||
|
"markdown",
|
||||||
|
"json",
|
||||||
|
"jsonc",
|
||||||
|
"yaml",
|
||||||
|
"toml",
|
||||||
|
"xml",
|
||||||
|
"gql",
|
||||||
|
"graphql",
|
||||||
|
"astro",
|
||||||
|
"css",
|
||||||
|
"less",
|
||||||
|
"scss",
|
||||||
|
"pcss",
|
||||||
|
"postcss"
|
||||||
|
]
|
||||||
|
}
|
||||||
81
README.md
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# BroadcastChannel
|
||||||
|
|
||||||
|
**Turn your Telegram Channel into a MicroBlog.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
English | [简体中文](./README.zh-cn.md)
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
- **Turn your Telegram Channel into a MicroBlog**
|
||||||
|
- **SEO friendly**
|
||||||
|
- **0 JS on the browser side**
|
||||||
|
- **RSS and RSS JSON**
|
||||||
|
|
||||||
|
## 🪧 Demo
|
||||||
|
|
||||||
|
BroadcastChannel supports deployment on serverless platforms like Cloudflare, Netlify, Vercel that support Node.js SSR, or on a VPS.
|
||||||
|
For detailed tutorials, see [Deploy your Astro site](https://docs.astro.build/en/guides/deploy/).
|
||||||
|
|
||||||
|
1. [Cloudflare](https://broadcast-channel.pages.dev/)
|
||||||
|
2. [Netlify](https://broadcast-channel.netlify.app/)
|
||||||
|
3. [Vercel](https://broadcast-channel.vercel.app/)
|
||||||
|
|
||||||
|
## 🧱 Tech Stack
|
||||||
|
|
||||||
|
- Framework: [Astro](https://astro.build/)
|
||||||
|
- CMS: [Telegram Channels](https://telegram.org/tour/channels)
|
||||||
|
- Template: [Sepia](https://github.com/Planetable/SiteTemplateSepia)
|
||||||
|
|
||||||
|
## 🏗️ Deployment
|
||||||
|
|
||||||
|
1. [Fork](https://github.com/ccbikai/BroadcastChannel/fork) this project to your Github
|
||||||
|
2. Create a project on Cloudflare/Netlify/Vercel
|
||||||
|
3. Select the `BroadcastChannel` project and the `Astro` framework
|
||||||
|
4. Configure the environment variable `CHANNEL` with your channel name. This is the minimal configuration, for more configurations see the options below
|
||||||
|
5. Save and deploy
|
||||||
|
6. Bind a domain (optional).
|
||||||
|
|
||||||
|
## ⚒️ Configuration
|
||||||
|
|
||||||
|
```env
|
||||||
|
## Telegram channel name, required
|
||||||
|
CHANNEL=Broadcast_Channel_Blog
|
||||||
|
|
||||||
|
## Language and timezone settings, language options see [dayjs](https://github.com/iamkun/dayjs/tree/dev/src/locale)
|
||||||
|
LOCALE=en
|
||||||
|
TIMEZONE="America/New_York"
|
||||||
|
|
||||||
|
## Social media usernames
|
||||||
|
TELEGRAM=ccbikai
|
||||||
|
TWITTER=ccbikai
|
||||||
|
GITHUB=ccbikai
|
||||||
|
|
||||||
|
## The following two social media need to be URLs
|
||||||
|
DISCORD=https://DISCORD.com
|
||||||
|
PODCAST=https://PODCAST.com
|
||||||
|
|
||||||
|
## Header and footer code injection, supports HTML
|
||||||
|
FOOTER_INJECT=FOOTER_INJECT
|
||||||
|
HEADER_INJECT=HEADER_INJECT
|
||||||
|
|
||||||
|
## SEO configuration options, can prevent search engines from indexing content
|
||||||
|
NO_FOLLOW=false
|
||||||
|
NO_INDEX=false
|
||||||
|
|
||||||
|
## Sentry configuration options, collect server-side errors
|
||||||
|
SENTRY_AUTH_TOKEN=SENTRY_AUTH_TOKEN
|
||||||
|
SENTRY_DSN=SENTRY_DSN
|
||||||
|
SENTRY_PROJECT=SENTRY_PROJECT
|
||||||
|
|
||||||
|
## Telegram host name and static resource proxy, not recommended to modify
|
||||||
|
HOST="telegram.dog"
|
||||||
|
STATIC_PROXY=""
|
||||||
|
```
|
||||||
|
|
||||||
|
## ☕ Sponsor
|
||||||
|
|
||||||
|
1. [Follow me on Telegram](https://t.me/miantiao_me)
|
||||||
|
2. [Follow me on 𝕏](https://x.com/0xKaiBi)
|
||||||
|
3. [Sponsor me on Github](https://github.com/sponsors/ccbikai)
|
||||||
81
README.zh-cn.md
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# 广播频道
|
||||||
|
|
||||||
|
**将你的 Telegram Channel 转为微博客。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[English](./README.md) | 简体中文
|
||||||
|
|
||||||
|
## ✨ 特性
|
||||||
|
|
||||||
|
- **将 Telegram Channel 转为微博客**
|
||||||
|
- **SEO 友好**
|
||||||
|
- **浏览器端 0 JS**
|
||||||
|
- **提供 RSS 和 RSS JSON**
|
||||||
|
|
||||||
|
## 🪧 演示
|
||||||
|
|
||||||
|
广播频道支持部署在 Cloudflare、Netlify、Vercel 等支持 Node.js SSR 的无服务器平台或者 VPS。
|
||||||
|
具体教程见[部署你的 Astro 站点](https://docs.astro.build/zh-cn/guides/deploy/)。
|
||||||
|
|
||||||
|
1. [Cloudflare](https://broadcast-channel.pages.dev/)
|
||||||
|
2. [Netlify](https://broadcast-channel.netlify.app/)
|
||||||
|
3. [Vercel](https://broadcast-channel.vercel.app/)
|
||||||
|
|
||||||
|
## 🧱 技术栈
|
||||||
|
|
||||||
|
- 框架:[Astro](https://astro.build/)
|
||||||
|
- 内容管理系统:[Telegram Channels](https://telegram.org/tour/channels)
|
||||||
|
- 模板: [Sepia](https://github.com/Planetable/SiteTemplateSepia)
|
||||||
|
|
||||||
|
## 🏗️ 部署
|
||||||
|
|
||||||
|
1. [Fork](https://github.com/ccbikai/BroadcastChannel/fork) 此项目到你 Github
|
||||||
|
2. 在 Cloudflare/Netlify/Vercel 创建项目
|
||||||
|
3. 选择 `BroadcastChannel` 项目和 `Astro` 框架
|
||||||
|
4. 配置环境变量 `CHANNEL` 为你的频道名称。此为最小化配置,更多配置见下面的配置项
|
||||||
|
5. 保存并部署
|
||||||
|
6. 绑定域名(可选)。
|
||||||
|
|
||||||
|
## ⚒️ 配置
|
||||||
|
|
||||||
|
```env
|
||||||
|
## Telegram 频道名称,必须配置
|
||||||
|
CHANNEL=Broadcast_Channel_Blog
|
||||||
|
|
||||||
|
## 语言和时区设置,语言选项见[dayjs](https://github.com/iamkun/dayjs/tree/dev/src/locale)
|
||||||
|
LOCALE=zh-cn
|
||||||
|
TIMEZONE="Asia/Shanghai"
|
||||||
|
|
||||||
|
## 社交媒体用户名
|
||||||
|
TELEGRAM=ccbikai
|
||||||
|
TWITTER=ccbikai
|
||||||
|
GITHUB=ccbikai
|
||||||
|
|
||||||
|
## 下面两个社交媒体需要为 URL
|
||||||
|
DISCORD=https://DISCORD.com
|
||||||
|
PODCASRT=https://PODCASRT.com
|
||||||
|
|
||||||
|
## 头部尾部代码注入,支持 HTML
|
||||||
|
FOOTER_INJECT=FOOTER_INJECT
|
||||||
|
HEADER_INJECT=HEADER_INJECT
|
||||||
|
|
||||||
|
## SEO 配置项,可不让搜索引擎索引内容
|
||||||
|
NO_FOLLOW=false
|
||||||
|
NO_INDEX=false
|
||||||
|
|
||||||
|
## Sentry 配置项,收集服务端报错
|
||||||
|
SENTRY_AUTH_TOKEN=SENTRY_AUTH_TOKEN
|
||||||
|
SENTRY_DSN=SENTRY_DSN
|
||||||
|
SENTRY_PROJECT=SENTRY_PROJECT
|
||||||
|
|
||||||
|
## Telegram 主机名称和静态资源代理,不建议修改
|
||||||
|
HOST="telegram.dog"
|
||||||
|
STATIC_PROXY=""
|
||||||
|
```
|
||||||
|
|
||||||
|
## ☕ 赞助
|
||||||
|
|
||||||
|
1. [在 Telegram 关注我](https://t.me/miantiao_me)
|
||||||
|
2. [在 𝕏 上关注我](https://x.com/ccbikai)
|
||||||
|
3. [在 Github 赞助我](https://github.com/sponsors/ccbikai)
|
||||||
86
astro.config.mjs
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import process from 'node:process'
|
||||||
|
import { defineConfig } from 'astro/config'
|
||||||
|
import vercel from '@astrojs/vercel/serverless'
|
||||||
|
import cloudflare from '@astrojs/cloudflare'
|
||||||
|
import netlify from '@astrojs/netlify'
|
||||||
|
import node from '@astrojs/node'
|
||||||
|
import { provider } from 'std-env'
|
||||||
|
import sentry from '@sentry/astro'
|
||||||
|
|
||||||
|
const providers = {
|
||||||
|
vercel: vercel({
|
||||||
|
edgeMiddleware: false,
|
||||||
|
}),
|
||||||
|
cloudflare_pages: cloudflare(),
|
||||||
|
netlify: netlify({
|
||||||
|
cacheOnDemandPages: true,
|
||||||
|
edgeMiddleware: false,
|
||||||
|
}),
|
||||||
|
node: node({
|
||||||
|
mode: 'standalone',
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const adapterProvider = process.env.SERVER_ADAPTER || provider
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
output: 'hybrid',
|
||||||
|
adapter: providers[adapterProvider] || providers.node,
|
||||||
|
integrations: [
|
||||||
|
...process.env.SENTRY_DSN
|
||||||
|
? [
|
||||||
|
sentry({
|
||||||
|
enabled: {
|
||||||
|
client: false,
|
||||||
|
server: process.env.SENTRY_DSN,
|
||||||
|
},
|
||||||
|
dsn: process.env.SENTRY_DSN,
|
||||||
|
sourceMapsUploadOptions: {
|
||||||
|
enabled: process.env.SENTRY_PROJECT && process.env.SENTRY_AUTH_TOKEN,
|
||||||
|
project: process.env.SENTRY_PROJECT,
|
||||||
|
authToken: process.env.SENTRY_AUTH_TOKEN,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
],
|
||||||
|
vite: {
|
||||||
|
ssr: {
|
||||||
|
external: [
|
||||||
|
...adapterProvider === 'cloudflare_pages'
|
||||||
|
? [
|
||||||
|
'module',
|
||||||
|
'url',
|
||||||
|
'events',
|
||||||
|
'worker_threads',
|
||||||
|
'async_hooks',
|
||||||
|
'node:diagnostics_channel',
|
||||||
|
'node:net',
|
||||||
|
'node:tls',
|
||||||
|
'node:worker_threads',
|
||||||
|
'node:util',
|
||||||
|
'node:fs',
|
||||||
|
'node:path',
|
||||||
|
'node:process',
|
||||||
|
'node:buffer',
|
||||||
|
'node:string_decoder',
|
||||||
|
'node:readline',
|
||||||
|
'node:events',
|
||||||
|
'node:stream',
|
||||||
|
'node:assert',
|
||||||
|
'node:os',
|
||||||
|
'node:crypto',
|
||||||
|
'node:zlib',
|
||||||
|
'node:http',
|
||||||
|
'node:https',
|
||||||
|
'node:url',
|
||||||
|
'node:querystring',
|
||||||
|
'node:child_process',
|
||||||
|
'node:inspector',
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
9
eslint.config.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import antfu from '@antfu/eslint-config'
|
||||||
|
|
||||||
|
export default antfu({
|
||||||
|
formatters: true,
|
||||||
|
astro: true,
|
||||||
|
rules: {
|
||||||
|
'no-console': ['error', { allow: ['info', 'warn', 'error'] }],
|
||||||
|
},
|
||||||
|
})
|
||||||
50
package.json
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"name": "broadcast-channel",
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"packageManager": "pnpm@9.5.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"start": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"astro": "astro",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint . --fix",
|
||||||
|
"postinstall": "simple-git-hooks"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/rss": "^4.0.7",
|
||||||
|
"@sentry/astro": "^8.22.0",
|
||||||
|
"astro": "^4.12.3",
|
||||||
|
"astro-seo": "^0.8.4",
|
||||||
|
"cheerio": "1.0.0-rc.12",
|
||||||
|
"dayjs": "^1.11.12",
|
||||||
|
"lru-cache": "^11.0.0",
|
||||||
|
"ofetch": "^1.3.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@antfu/eslint-config": "^2.24.1",
|
||||||
|
"@astrojs/cloudflare": "^11.0.1",
|
||||||
|
"@astrojs/netlify": "^5.4.0",
|
||||||
|
"@astrojs/node": "^8.3.2",
|
||||||
|
"@astrojs/vercel": "^7.7.2",
|
||||||
|
"astro-eslint-parser": "^1.0.2",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"cssnano": "^7.0.4",
|
||||||
|
"eslint": "9.5.0",
|
||||||
|
"eslint-plugin-astro": "^1.2.3",
|
||||||
|
"eslint-plugin-format": "^0.1.2",
|
||||||
|
"lint-staged": "^15.2.8",
|
||||||
|
"prettier-plugin-astro": "^0.14.1",
|
||||||
|
"simple-git-hooks": "^2.11.1",
|
||||||
|
"std-env": "^3.7.0"
|
||||||
|
},
|
||||||
|
"simple-git-hooks": {
|
||||||
|
"pre-commit": "pnpm lint-staged"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*": "eslint --fix"
|
||||||
|
}
|
||||||
|
}
|
||||||
9912
pnpm-lock.yaml
generated
Normal file
6
postcss.config.cjs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: [
|
||||||
|
require('autoprefixer'),
|
||||||
|
require('cssnano'),
|
||||||
|
],
|
||||||
|
}
|
||||||
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
1
public/favicon.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink"><path fill="currentColor" d="M5.719 14.75a.997.997 0 0 1-.664-.252L-.005 10l5.341-4.748a1 1 0 0 1 1.328 1.495L3.005 10l3.378 3.002a1 1 0 0 1-.664 1.748zm8.945-.002L20.005 10l-5.06-4.498a.999.999 0 1 0-1.328 1.495L16.995 10l-3.659 3.252a1 1 0 0 0 1.328 1.496zm-4.678 1.417 2-12a1 1 0 1 0-1.972-.329l-2 12a1 1 0 1 0 1.972.329z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 457 B |
6
src/assets/discord.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<title>discord</title>
|
||||||
|
<path d="M20.992 20.163c-1.511-0.099-2.699-1.349-2.699-2.877 0-0.051 0.001-0.102 0.004-0.153l-0 0.007c-0.003-0.048-0.005-0.104-0.005-0.161 0-1.525 1.19-2.771 2.692-2.862l0.008-0c1.509 0.082 2.701 1.325 2.701 2.847 0 0.062-0.002 0.123-0.006 0.184l0-0.008c0.003 0.050 0.005 0.109 0.005 0.168 0 1.523-1.191 2.768-2.693 2.854l-0.008 0zM11.026 20.163c-1.511-0.099-2.699-1.349-2.699-2.877 0-0.051 0.001-0.102 0.004-0.153l-0 0.007c-0.003-0.048-0.005-0.104-0.005-0.161 0-1.525 1.19-2.771 2.692-2.862l0.008-0c1.509 0.082 2.701 1.325 2.701 2.847 0 0.062-0.002 0.123-0.006 0.184l0-0.008c0.003 0.048 0.005 0.104 0.005 0.161 0 1.525-1.19 2.771-2.692 2.862l-0.008 0zM26.393 6.465c-1.763-0.832-3.811-1.49-5.955-1.871l-0.149-0.022c-0.005-0.001-0.011-0.002-0.017-0.002-0.035 0-0.065 0.019-0.081 0.047l-0 0c-0.234 0.411-0.488 0.924-0.717 1.45l-0.043 0.111c-1.030-0.165-2.218-0.259-3.428-0.259s-2.398 0.094-3.557 0.275l0.129-0.017c-0.27-0.63-0.528-1.142-0.813-1.638l0.041 0.077c-0.017-0.029-0.048-0.047-0.083-0.047-0.005 0-0.011 0-0.016 0.001l0.001-0c-2.293 0.403-4.342 1.060-6.256 1.957l0.151-0.064c-0.017 0.007-0.031 0.019-0.040 0.034l-0 0c-2.854 4.041-4.562 9.069-4.562 14.496 0 0.907 0.048 1.802 0.141 2.684l-0.009-0.11c0.003 0.029 0.018 0.053 0.039 0.070l0 0c2.14 1.601 4.628 2.891 7.313 3.738l0.176 0.048c0.008 0.003 0.018 0.004 0.028 0.004 0.032 0 0.060-0.015 0.077-0.038l0-0c0.535-0.72 1.044-1.536 1.485-2.392l0.047-0.1c0.006-0.012 0.010-0.027 0.010-0.043 0-0.041-0.026-0.075-0.062-0.089l-0.001-0c-0.912-0.352-1.683-0.727-2.417-1.157l0.077 0.042c-0.029-0.017-0.048-0.048-0.048-0.083 0-0.031 0.015-0.059 0.038-0.076l0-0c0.157-0.118 0.315-0.24 0.465-0.364 0.016-0.013 0.037-0.021 0.059-0.021 0.014 0 0.027 0.003 0.038 0.008l-0.001-0c2.208 1.061 4.8 1.681 7.536 1.681s5.329-0.62 7.643-1.727l-0.107 0.046c0.012-0.006 0.025-0.009 0.040-0.009 0.022 0 0.043 0.008 0.059 0.021l-0-0c0.15 0.124 0.307 0.248 0.466 0.365 0.023 0.018 0.038 0.046 0.038 0.077 0 0.035-0.019 0.065-0.046 0.082l-0 0c-0.661 0.395-1.432 0.769-2.235 1.078l-0.105 0.036c-0.036 0.014-0.062 0.049-0.062 0.089 0 0.016 0.004 0.031 0.011 0.044l-0-0.001c0.501 0.96 1.009 1.775 1.571 2.548l-0.040-0.057c0.017 0.024 0.046 0.040 0.077 0.040 0.010 0 0.020-0.002 0.029-0.004l-0.001 0c2.865-0.892 5.358-2.182 7.566-3.832l-0.065 0.047c0.022-0.016 0.036-0.041 0.039-0.069l0-0c0.087-0.784 0.136-1.694 0.136-2.615 0-5.415-1.712-10.43-4.623-14.534l0.052 0.078c-0.008-0.016-0.022-0.029-0.038-0.036l-0-0z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.7 KiB |
3
src/assets/github.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="64px" height="64px" viewBox="0 0 64 64" id="i-github" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path stroke-width="0" fill="currentColor" d="M32 0 C14 0 0 14 0 32 0 53 19 62 22 62 24 62 24 61 24 60 L24 55 C17 57 14 53 13 50 13 50 13 49 11 47 10 46 6 44 10 44 13 44 15 48 15 48 18 52 22 51 24 50 24 48 26 46 26 46 18 45 12 42 12 31 12 27 13 24 15 22 15 22 13 18 15 13 15 13 20 13 24 17 27 15 37 15 40 17 44 13 49 13 49 13 51 20 49 22 49 22 51 24 52 27 52 31 52 42 45 45 38 46 39 47 40 49 40 52 L40 60 C40 61 40 62 42 62 45 62 64 53 64 32 64 14 50 0 32 0 Z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 576 B |
117
src/assets/global.css
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
@view-transition {
|
||||||
|
navigation: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-title {
|
||||||
|
view-transition-name: site-title;
|
||||||
|
transition: 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
transition: 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
[popover]:not(:popover-open):not(dialog[open]) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview-wrap {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0px;
|
||||||
|
left: 0px;
|
||||||
|
z-index: 1000;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-img {
|
||||||
|
margin: auto;
|
||||||
|
max-width: calc(100% - 40px);
|
||||||
|
max-height: calc(100% - 40px);
|
||||||
|
border-radius: var(--media-border-radius);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
box-shadow: var(--shadows);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.search-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: 0px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
padding: 4px;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '🔍';
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon:checked + .search-form {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
.search-form {
|
||||||
|
display: none;
|
||||||
|
background: rgba(255, 255, 255, 0.75);
|
||||||
|
padding: 8px;
|
||||||
|
position: sticky;
|
||||||
|
top: 60px;
|
||||||
|
border-radius: var(--box-border-radius);
|
||||||
|
|
||||||
|
> input {
|
||||||
|
border: 1px solid var(--background-color);
|
||||||
|
border-radius: var(--box-border-radius);
|
||||||
|
outline: none;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 2.4;
|
||||||
|
padding: 0 0.5em;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyright-wrap {
|
||||||
|
position: sticky;
|
||||||
|
top: 120px;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-size: 14px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 600px) {
|
||||||
|
.search-form {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyright-wrap {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
351
src/assets/normalize.css
vendored
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
|
||||||
|
|
||||||
|
/* Document
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the line height in all browsers.
|
||||||
|
* 2. Prevent adjustments of font size after orientation changes in iOS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
html {
|
||||||
|
line-height: 1.15; /* 1 */
|
||||||
|
-webkit-text-size-adjust: 100%; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sections
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the margin in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the `main` element consistently in IE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
main {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the font size and margin on `h1` elements within `section` and
|
||||||
|
* `article` contexts in Chrome, Firefox, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
margin: 0.67em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grouping content
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Add the correct box sizing in Firefox.
|
||||||
|
* 2. Show the overflow in Edge and IE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
hr {
|
||||||
|
box-sizing: content-box; /* 1 */
|
||||||
|
height: 0; /* 1 */
|
||||||
|
overflow: visible; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||||
|
* 2. Correct the odd `em` font sizing in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
pre {
|
||||||
|
font-family: monospace, monospace; /* 1 */
|
||||||
|
font-size: 1em; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text-level semantics
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the gray background on active links in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
a {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Remove the bottom border in Chrome 57-
|
||||||
|
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
abbr[title] {
|
||||||
|
border-bottom: none; /* 1 */
|
||||||
|
text-decoration: underline; /* 2 */
|
||||||
|
text-decoration: underline dotted; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct font weight in Chrome, Edge, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||||
|
* 2. Correct the odd `em` font sizing in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
samp {
|
||||||
|
font-family: monospace, monospace; /* 1 */
|
||||||
|
font-size: 1em; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct font size in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent `sub` and `sup` elements from affecting the line height in
|
||||||
|
* all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
font-size: 75%;
|
||||||
|
line-height: 0;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Embedded content
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the border on images inside links in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Forms
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Change the font styles in all browsers.
|
||||||
|
* 2. Remove the margin in Firefox and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
optgroup,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
font-family: inherit; /* 1 */
|
||||||
|
font-size: 100%; /* 1 */
|
||||||
|
line-height: 1.15; /* 1 */
|
||||||
|
margin: 0; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the overflow in IE.
|
||||||
|
* 1. Show the overflow in Edge.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input {
|
||||||
|
/* 1 */
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the inheritance of text transform in Edge, Firefox, and IE.
|
||||||
|
* 1. Remove the inheritance of text transform in Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
select {
|
||||||
|
/* 1 */
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
[type='button'],
|
||||||
|
[type='reset'],
|
||||||
|
[type='submit'] {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the inner border and padding in Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button::-moz-focus-inner,
|
||||||
|
[type='button']::-moz-focus-inner,
|
||||||
|
[type='reset']::-moz-focus-inner,
|
||||||
|
[type='submit']::-moz-focus-inner {
|
||||||
|
border-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore the focus styles unset by the previous rule.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button:-moz-focusring,
|
||||||
|
[type='button']:-moz-focusring,
|
||||||
|
[type='reset']:-moz-focusring,
|
||||||
|
[type='submit']:-moz-focusring {
|
||||||
|
outline: 1px dotted ButtonText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the padding in Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
padding: 0.35em 0.75em 0.625em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the text wrapping in Edge and IE.
|
||||||
|
* 2. Correct the color inheritance from `fieldset` elements in IE.
|
||||||
|
* 3. Remove the padding so developers are not caught out when they zero out
|
||||||
|
* `fieldset` elements in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
legend {
|
||||||
|
box-sizing: border-box; /* 1 */
|
||||||
|
color: inherit; /* 2 */
|
||||||
|
display: table; /* 1 */
|
||||||
|
max-width: 100%; /* 1 */
|
||||||
|
padding: 0; /* 3 */
|
||||||
|
white-space: normal; /* 1 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
|
||||||
|
*/
|
||||||
|
|
||||||
|
progress {
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the default vertical scrollbar in IE 10+.
|
||||||
|
*/
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Add the correct box sizing in IE 10.
|
||||||
|
* 2. Remove the padding in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type='checkbox'],
|
||||||
|
[type='radio'] {
|
||||||
|
box-sizing: border-box; /* 1 */
|
||||||
|
padding: 0; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the cursor style of increment and decrement buttons in Chrome.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type='number']::-webkit-inner-spin-button,
|
||||||
|
[type='number']::-webkit-outer-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the odd appearance in Chrome and Safari.
|
||||||
|
* 2. Correct the outline style in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type='search'] {
|
||||||
|
-webkit-appearance: textfield; /* 1 */
|
||||||
|
outline-offset: -2px; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the inner padding in Chrome and Safari on macOS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type='search']::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
* 2. Change font properties to `inherit` in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
::-webkit-file-upload-button {
|
||||||
|
-webkit-appearance: button; /* 1 */
|
||||||
|
font: inherit; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Interactive
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add the correct display in Edge, IE 10+, and Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
details {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add the correct display in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
summary {
|
||||||
|
display: list-item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Misc
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct display in IE 10+.
|
||||||
|
*/
|
||||||
|
|
||||||
|
template {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct display in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
16
src/assets/podcast.svg
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="11" r="1" />
|
||||||
|
<path d="M17.03 18.46a9 9 0 10-10.02.03" />
|
||||||
|
<path d="M16.06 13.91a5 5 0 10-7.97.2" />
|
||||||
|
<path d="M11.11 17a.9.9 0 111.78 0l-.52 4.67a.37.37 0 01-.74 0l-.52-4.68z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 429 B |
13
src/assets/rss.svg
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 310 310" style="enable-background:new 0 0 310 310;" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<path d="M90.244,264.828C90.244,240.11,70.139,220,45.427,220c-24.715,0-44.822,20.11-44.822,44.828
|
||||||
|
c0,24.714,20.107,44.82,44.822,44.82C70.139,309.648,90.244,289.542,90.244,264.828z"/>
|
||||||
|
<path d="M5.648,169.43c35.961,0,69.782,14.066,95.231,39.605c25.45,25.583,39.467,59.648,39.467,95.92
|
||||||
|
c0,2.762,2.238,5,5,5h57.486c2.762,0,5-2.238,5-5c0-111.952-90.699-203.031-202.185-203.031c-2.762,0-5,2.238-5,5v57.505
|
||||||
|
C0.648,167.191,2.887,169.43,5.648,169.43z"/>
|
||||||
|
<path id="XMLID_791_" d="M5.726,0c-2.762,0-5,2.238-5,5v57.495c0,2.762,2.238,5,5,5c130.24,0,236.199,106.544,236.199,237.505
|
||||||
|
c0,2.762,2.238,5,5,5h57.471c2.762,0,5-2.238,5-5C309.396,136.822,173.17,0,5.726,0z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 952 B |
890
src/assets/style.css
Normal file
@@ -0,0 +1,890 @@
|
|||||||
|
:root {
|
||||||
|
--background-color: #f4f1ec;
|
||||||
|
--foreground-color: #000000;
|
||||||
|
--highlight-color: orangered;
|
||||||
|
--box-border-radius: 3px;
|
||||||
|
--media-border-radius: 8px;
|
||||||
|
--dot-size: 8px;
|
||||||
|
--shadows: 0 1px 2px rgba(0, 0, 0, 0.02), 0 2px 4px rgba(0, 0, 0, 0.02),
|
||||||
|
0 4px 8px rgba(0, 0, 0, 0.02), 0 8px 16px rgba(0, 0, 0, 0.02);
|
||||||
|
--box-margin: 10px;
|
||||||
|
--border-color: rgba(0, 0, 0, 0.05);
|
||||||
|
--link-color: var(--highlight-color);
|
||||||
|
--icon-hover-filter: invert(51%) sepia(0%) saturate(0%) hue-rotate(23deg)
|
||||||
|
brightness(90%) contrast(90%);
|
||||||
|
--icon-secondary-filter: invert(97%) sepia(0%) saturate(0%) hue-rotate(129deg)
|
||||||
|
brightness(86%) contrast(88%);
|
||||||
|
--cell-background-color: #fff;
|
||||||
|
--code-background-color: #f9f9f9;
|
||||||
|
--secondary-color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--background-color);
|
||||||
|
color: var(--foreground-color);
|
||||||
|
font-family:
|
||||||
|
ui-sans-serif,
|
||||||
|
system-ui,
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
Segoe UI,
|
||||||
|
Roboto,
|
||||||
|
Helvetica Neue,
|
||||||
|
Arial,
|
||||||
|
Noto Sans,
|
||||||
|
sans-serif,
|
||||||
|
Apple Color Emoji,
|
||||||
|
Segoe UI Emoji,
|
||||||
|
Segoe UI Symbol,
|
||||||
|
Noto Color Emoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
#wrapper {
|
||||||
|
margin-left: 20px;
|
||||||
|
margin-right: 20px;
|
||||||
|
margin-top: 0px;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0px;
|
||||||
|
left: 0px;
|
||||||
|
z-index: 1000;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#modal-img {
|
||||||
|
margin: auto;
|
||||||
|
max-width: calc(100% - 40px);
|
||||||
|
max-height: calc(100% - 40px);
|
||||||
|
border-radius: var(--media-border-radius);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
box-shadow: var(--shadows);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:link,
|
||||||
|
a:visited {
|
||||||
|
color: #778087;
|
||||||
|
text-decoration: none;
|
||||||
|
line-break: loose;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #4d5256;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.site-title:link,
|
||||||
|
a.site-title:visited {
|
||||||
|
color: #333;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.site-title:hover {
|
||||||
|
color: #000;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.item-link:link,
|
||||||
|
a.item-link:visited {
|
||||||
|
color: var(--link-color);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.item-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
#container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0px auto 0px auto;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main-container {
|
||||||
|
flex: 1;
|
||||||
|
padding-top: 20px;
|
||||||
|
padding-right: 30px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
margin-right: 20px;
|
||||||
|
border-right: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#header {
|
||||||
|
padding: 10px 10px 10px 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 100%;
|
||||||
|
border: 3px solid #fff;
|
||||||
|
box-shadow: var(--shadows);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 20px;
|
||||||
|
margin-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-icons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-icon {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
vertical-align: bottom;
|
||||||
|
filter: var(--icon-secondary-filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-icon:hover {
|
||||||
|
filter: var(--icon-hover-filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
#breadcrumb {
|
||||||
|
padding: 10px 0px 10px 0px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-avatar {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 100%;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
box-shadow: var(--shadows);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-title {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: none;
|
||||||
|
height: 2px;
|
||||||
|
color: var(--border-color);
|
||||||
|
background-color: var(--border-color);
|
||||||
|
margin-top: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
padding: 10px 10px 10px 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main-content {
|
||||||
|
font-size: 17px;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#site-intro {
|
||||||
|
padding: 10px 20px 10px 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.6;
|
||||||
|
border-radius: var(--box-border-radius);
|
||||||
|
box-shadow: var(--shadows);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0px 20px 0px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items {
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-left: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-box,
|
||||||
|
.video-box,
|
||||||
|
.audio-box,
|
||||||
|
.attachment-box {
|
||||||
|
border-left: 2px solid var(--border-color);
|
||||||
|
padding: 30px 0px 30px 30px;
|
||||||
|
display: flex;
|
||||||
|
margin-left: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-box {
|
||||||
|
box-sizing: border-box;
|
||||||
|
max-width: 100%;
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-box img {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1/1;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: var(--media-border-radius);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
box-shadow: var(--shadows);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-box > img {
|
||||||
|
display: block;
|
||||||
|
width: calc(100% - 1px);
|
||||||
|
height: auto;
|
||||||
|
max-height: initial;
|
||||||
|
border-radius: var(--media-border-radius);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
box-shadow: var(--shadows);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-box video {
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: var(--media-border-radius);
|
||||||
|
box-shadow: var(--shadows);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
audio::-webkit-media-controls-play-button {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
audio::-webkit-media-controls-panel {
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-box audio {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: var(--media-border-radius);
|
||||||
|
box-shadow: var(--shadows);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-box {
|
||||||
|
border-left: 2px solid var(--border-color);
|
||||||
|
padding: 30px 0px 0px 30px;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1.2;
|
||||||
|
font-weight: 800;
|
||||||
|
margin-left: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-box:last-child {
|
||||||
|
padding-top: 30px;
|
||||||
|
padding-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-box {
|
||||||
|
border-left: 2px solid var(--border-color);
|
||||||
|
padding: 0px 0px 30px 30px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-left: 3px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-box:last-child {
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-box {
|
||||||
|
border-left: 2px solid var(--border-color);
|
||||||
|
padding: 30px 0px 30px 30px;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-left: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-box p:first-child {
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-box p:last-child {
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-box {
|
||||||
|
padding: 0px;
|
||||||
|
line-height: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-box > .dot {
|
||||||
|
width: var(--dot-size);
|
||||||
|
height: var(--dot-size);
|
||||||
|
border-radius: var(--dot-size);
|
||||||
|
background-color: var(--link-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-box > .time {
|
||||||
|
flex: 1;
|
||||||
|
color: var(--link-color);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#aside-container {
|
||||||
|
padding-bottom: 20px;
|
||||||
|
width: 200px;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#aside-container .nav {
|
||||||
|
padding-top: 20px;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: var(--box-margin);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-icon {
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:link,
|
||||||
|
.nav-link:visited {
|
||||||
|
flex: 1;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #333;
|
||||||
|
padding: 5px 10px 5px 10px;
|
||||||
|
border-radius: var(--box-border-radius);
|
||||||
|
display: inline-block;
|
||||||
|
transition:
|
||||||
|
background-color 0.15s ease-in-out,
|
||||||
|
box-shadow 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link.current {
|
||||||
|
background-color: rgba(255, 255, 255, 0.75);
|
||||||
|
box-shadow: var(--shadows);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.65);
|
||||||
|
box-shadow: var(--shadows);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* START: archive */
|
||||||
|
|
||||||
|
.archive-container {
|
||||||
|
margin: 0px auto 40px auto;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
padding-bottom: 1em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive-title {
|
||||||
|
font-size: 2em;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive-count {
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: var(--foreground-secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive-list {
|
||||||
|
column-count: 2;
|
||||||
|
column-rule: 1px solid var(--border-color);
|
||||||
|
column-gap: 40px;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive-list-item {
|
||||||
|
display: block;
|
||||||
|
font-size: 1em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5em;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive-list-header {
|
||||||
|
display: block;
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
color: var(--foreground-secondary-color);
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive-list-header:first-child {
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive-list-item a {
|
||||||
|
line-break: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* END: archive */
|
||||||
|
|
||||||
|
/* START: tag */
|
||||||
|
|
||||||
|
.tag-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background-size: 16px 16px;
|
||||||
|
opacity: 0.25;
|
||||||
|
background-image: url('tags.png');
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag:link,
|
||||||
|
.tag:visited {
|
||||||
|
color: var(--secondary-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 10px 2px 10px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag:hover {
|
||||||
|
color: var(--link-color);
|
||||||
|
border-color: var(--link-color);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-container {
|
||||||
|
margin: 0px auto 40px auto;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
padding-bottom: 1em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-caption {
|
||||||
|
font-size: 0.8em;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5em;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-title {
|
||||||
|
font-size: 2em;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-count {
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: var(--foreground-secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-container {
|
||||||
|
font-size: 0.8em;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1em;
|
||||||
|
color: var(--foreground-secondary-color);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5em;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-cloud {
|
||||||
|
column-count: 2;
|
||||||
|
column-rule: 1px solid var(--border-color);
|
||||||
|
column-gap: 40px;
|
||||||
|
line-height: 2;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-cloud-item {
|
||||||
|
display: block;
|
||||||
|
font-size: 1em;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-cloud-item-count {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: var(--border-color);
|
||||||
|
font-size: 0.75em;
|
||||||
|
color: var(--background-color);
|
||||||
|
padding: 1px 4px 1px 4px;
|
||||||
|
border-radius: 20px;
|
||||||
|
line-height: 1;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-item {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.2em 0.5em 0.2em 0.5em;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-item:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
border-color: var(--link-color);
|
||||||
|
box-shadow: 0px 1px 2px var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* END: tag */
|
||||||
|
|
||||||
|
/* START: Markdown tags */
|
||||||
|
|
||||||
|
.content h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content h2 {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-top: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-top: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content h4 {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content h5 {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content h6 {
|
||||||
|
font-size: 10px;
|
||||||
|
margin-top: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content ul:first-child,
|
||||||
|
.content ol:first-child,
|
||||||
|
.content p:first-child,
|
||||||
|
.content h1:first-child,
|
||||||
|
.content h2:first-child,
|
||||||
|
.content h3:first-child,
|
||||||
|
.content h4:first-child,
|
||||||
|
.content h5:first-child,
|
||||||
|
.content h6:first-child {
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content ul:last-child,
|
||||||
|
.content ol:last-child,
|
||||||
|
.content p:last-child,
|
||||||
|
.content h1:last-child,
|
||||||
|
.content h2:last-child,
|
||||||
|
.content h3:last-child,
|
||||||
|
.content h4:last-child,
|
||||||
|
.content h5:last-child,
|
||||||
|
.content h6:last-child {
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content li {
|
||||||
|
line-break: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content img {
|
||||||
|
max-width: calc(100% - 1px);
|
||||||
|
max-height: initial;
|
||||||
|
border-radius: var(--media-border-radius);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
box-shadow: var(--shadows);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content a:link,
|
||||||
|
.content a:visited {
|
||||||
|
line-break: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content pre {
|
||||||
|
font-size: 0.9em;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: var(--media-border-radius);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
box-shadow: var(--shadows);
|
||||||
|
background-color: rgba(255, 255, 240, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content code {
|
||||||
|
line-break: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content figure {
|
||||||
|
margin-inline-start: 0px;
|
||||||
|
margin-inline-end: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content figcaption {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content > iframe {
|
||||||
|
max-width: calc(100% - 1px);
|
||||||
|
border-radius: var(--media-border-radius);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
box-shadow: var(--shadows);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* END: Markdown tags */
|
||||||
|
|
||||||
|
/* START: pages */
|
||||||
|
|
||||||
|
.pages-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pages-info {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
color: var(--secondary-color);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.page:link,
|
||||||
|
a.page:visited {
|
||||||
|
color: var(--secondary-color);
|
||||||
|
padding: 5px 15px 5px 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 30px;
|
||||||
|
border: 1px solid var(--secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
a.page:hover {
|
||||||
|
color: var(--link-color);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-placeholder {
|
||||||
|
width: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* END: pages */
|
||||||
|
|
||||||
|
/* START: table */
|
||||||
|
|
||||||
|
.content table {
|
||||||
|
table-layout: auto;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
border-collapse: collapse;
|
||||||
|
box-shadow: var(--shadows);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content table tr {
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
background-color: var(--cell-background-color);
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content table tr:nth-child(2n) {
|
||||||
|
background-color: var(--code-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content table tr th {
|
||||||
|
font-weight: bold;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
margin: 0;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background-color: var(--code-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content table tr td {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
margin: 0;
|
||||||
|
padding: 6px 12px;
|
||||||
|
line-break: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content table tr th :first-child,
|
||||||
|
.content table tr td :first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content table tr th :last-child,
|
||||||
|
.content table tr td :last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* END: table */
|
||||||
|
|
||||||
|
/* START: To Do Items */
|
||||||
|
|
||||||
|
ul:has(input[type='checkbox']) {
|
||||||
|
padding-inline-start: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li input[type='checkbox'] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
li:has(input[type='checkbox']) {
|
||||||
|
list-style-type: none;
|
||||||
|
margin-left: 0px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
li:has(input[type='checkbox']:not(:checked):disabled)::before {
|
||||||
|
background-image: url('./circle.svg');
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: text-top;
|
||||||
|
width: 1.25em;
|
||||||
|
height: 1em;
|
||||||
|
background-size: 1em 1em;
|
||||||
|
margin-top: 2px;
|
||||||
|
filter: var(--icon-hover-filter);
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
li:has(input[type='checkbox']:checked:disabled)::before {
|
||||||
|
background-image: url('./checkmark.circle.fill.svg');
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: text-top;
|
||||||
|
width: 1.25em;
|
||||||
|
height: 1em;
|
||||||
|
background-size: 1em 1em;
|
||||||
|
margin-top: 1px;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* END: To Do Items */
|
||||||
|
|
||||||
|
@media screen and (max-width: 799px) {
|
||||||
|
#container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
#container {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main-container {
|
||||||
|
padding-right: 0px;
|
||||||
|
margin-right: 0px;
|
||||||
|
width: 100%;
|
||||||
|
border-right: none;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header {
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items {
|
||||||
|
margin-left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#aside-container {
|
||||||
|
width: 100%;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
padding-bottom: 10px;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
box-shadow: 0px 4px 16px -16px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#aside-container .nav {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#aside-container .nav-item {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
padding: 10px 0px 0px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-cloud {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-cloud-item {
|
||||||
|
padding-left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive-list {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive-list-item {
|
||||||
|
padding-left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive-list-header {
|
||||||
|
padding-left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#site-intro {
|
||||||
|
margin-left: 0px;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
src/assets/tags.png
Normal file
|
After Width: | Height: | Size: 882 B |
1
src/assets/telegram.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg width="24px" height="24px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m20.665 3.717-17.73 6.837c-1.21.486-1.203 1.161-.222 1.462l4.552 1.42 10.532-6.645c.498-.303.953-.14.579.192l-8.533 7.701h-.002l.002.001-.314 4.692c.46 0 .663-.211.921-.46l2.211-2.15 4.599 3.397c.848.467 1.457.227 1.668-.785l3.019-14.228c.309-1.239-.473-1.8-1.282-1.434z"/></svg>
|
||||||
|
After Width: | Height: | Size: 375 B |
3
src/assets/twitter.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="64px" height="64px" viewBox="0 0 64 64" id="i-twitter" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path stroke-width="0" fill="currentColor" d="M60 16 L54 17 L58 12 L51 14 C42 4 28 15 32 24 C16 24 8 12 8 12 C8 12 2 21 12 28 L6 26 C6 32 10 36 17 38 L10 38 C14 46 21 46 21 46 C21 46 15 51 4 51 C37 67 57 37 54 21 Z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 332 B |
BIN
src/assets/void.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
135
src/components/header.astro
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
---
|
||||||
|
import { getEnv } from '../lib/env'
|
||||||
|
import voidFile from '../assets/void.png'
|
||||||
|
import rss from '../assets/rss.svg'
|
||||||
|
import podcast from '../assets/podcast.svg'
|
||||||
|
import twitter from '../assets/twitter.svg'
|
||||||
|
import github from '../assets/github.svg'
|
||||||
|
import discord from '../assets/discord.svg'
|
||||||
|
import telegram from '../assets/telegram.svg'
|
||||||
|
|
||||||
|
const { SITE_URL } = Astro.locals
|
||||||
|
const { channel } = Astro.props
|
||||||
|
|
||||||
|
const PODCASRT = getEnv(import.meta.env, Astro, 'PODCASRT')
|
||||||
|
const TWITTER = getEnv(import.meta.env, Astro, 'TWITTER')
|
||||||
|
const GITHUB = getEnv(import.meta.env, Astro, 'GITHUB')
|
||||||
|
const TELEGRAM = getEnv(import.meta.env, Astro, 'TELEGRAM')
|
||||||
|
const DISCORD = getEnv(import.meta.env, Astro, 'DISCORD')
|
||||||
|
|
||||||
|
const staticProxy = getEnv(import.meta.env, Astro, 'STATIC_PROXY') ?? '/static/'
|
||||||
|
---
|
||||||
|
|
||||||
|
<div id="header">
|
||||||
|
<a href={SITE_URL} title={channel?.title}>
|
||||||
|
<img
|
||||||
|
src={channel?.avatar?.startsWith('http')
|
||||||
|
? staticProxy + channel?.avatar
|
||||||
|
: voidFile.src}
|
||||||
|
alt={channel?.title}
|
||||||
|
class="header-avatar"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<div class="header-title">
|
||||||
|
<a href={SITE_URL} class="site-title" title={channel?.title}>
|
||||||
|
{channel?.title}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="header-icons">
|
||||||
|
<a
|
||||||
|
href={`${SITE_URL}rss.xml`}
|
||||||
|
target="_blank"
|
||||||
|
rel="alternate"
|
||||||
|
type="application/rss+xml"
|
||||||
|
title="RSS Feed"
|
||||||
|
>
|
||||||
|
<img {...rss} alt="RSS" class="social-icon" width="1em" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{
|
||||||
|
PODCASRT && (
|
||||||
|
<a href={PODCASRT} target="_blank" title="Podcast">
|
||||||
|
<img {...podcast} alt="Podcast" class="social-icon" width="1em" />
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
TWITTER && TWITTER.length > 0 && (
|
||||||
|
<a
|
||||||
|
href={`https://twitter.com/${TWITTER}`}
|
||||||
|
title="Twitter"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
{...twitter}
|
||||||
|
alt={`twitter.com/${TWITTER}`}
|
||||||
|
class="social-icon"
|
||||||
|
width="1em"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
GITHUB && GITHUB.length > 0 && (
|
||||||
|
<a href={`https://github.com/${GITHUB}`} title="Github" target="_blank">
|
||||||
|
<img
|
||||||
|
{...github}
|
||||||
|
alt={`github.com/${GITHUB}`}
|
||||||
|
class="social-icon"
|
||||||
|
width="1em"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
TELEGRAM && TELEGRAM.length > 0 && (
|
||||||
|
<a href={`https://t.me/${TELEGRAM}`} title="Telegram" target="_blank">
|
||||||
|
<img
|
||||||
|
{...telegram}
|
||||||
|
alt={`t.me/${TELEGRAM}`}
|
||||||
|
class="social-icon"
|
||||||
|
width="1em"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
DISCORD && DISCORD.length > 0 && (
|
||||||
|
<a href={DISCORD} title="Discord" target="_blank">
|
||||||
|
<img
|
||||||
|
{...discord}
|
||||||
|
alt="Discord Invite"
|
||||||
|
class="social-icon"
|
||||||
|
width="1em"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
channel?.description && channel?.description.length > 0 && (
|
||||||
|
<div class="text-box" id="site-intro">
|
||||||
|
{channel?.description}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#site-intro {
|
||||||
|
color: var(--secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-icon {
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-icons {
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
177
src/components/item.astro
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
---
|
||||||
|
import dayjs from '../lib/dayjs'
|
||||||
|
import { getEnv } from '../lib/env'
|
||||||
|
|
||||||
|
const locale = getEnv(import.meta.env, Astro, 'LOCALE')
|
||||||
|
const timezone = getEnv(import.meta.env, Astro, 'TIMEZONE')
|
||||||
|
|
||||||
|
locale && dayjs.locale(locale)
|
||||||
|
|
||||||
|
const { SITE_URL } = Astro.locals
|
||||||
|
const { post } = Astro.props
|
||||||
|
|
||||||
|
const datetime = dayjs(post.datetime).tz(timezone)
|
||||||
|
const timeago = datetime.isBefore(dayjs().subtract(1, 'w'))
|
||||||
|
? datetime.format('HH:mm · ll · ddd')
|
||||||
|
: datetime.fromNow()
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="item" style={{ 'view-transition-name': `post-${post.id}` }}>
|
||||||
|
<div class="time-box">
|
||||||
|
<div class="dot"></div>
|
||||||
|
<div class="time">
|
||||||
|
<a
|
||||||
|
href={`${SITE_URL}posts/${post.id}`}
|
||||||
|
title={post.datetime}
|
||||||
|
class="item-link">{timeago}</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
post.content.length > 0 && (
|
||||||
|
<div class={`text-box content`} set:html={post.content} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
post.tags.length > 0 && (
|
||||||
|
<div
|
||||||
|
class="tag-box"
|
||||||
|
style={post.content.length === 0 ? 'padding-top: 30px;' : ''}
|
||||||
|
>
|
||||||
|
<div class="tag-icon" />
|
||||||
|
{post.tags.map((tag) => (
|
||||||
|
<a href={`/search/%23${tag}`} title={tag} class="tag">
|
||||||
|
{tag}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.content {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.content :global(img) {
|
||||||
|
width: calc(100% - var(--box-margin));
|
||||||
|
}
|
||||||
|
.content :global(.tgme_widget_message_link_preview) {
|
||||||
|
margin-top: 16px;
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
.link_preview_site_name,
|
||||||
|
.link_preview_title,
|
||||||
|
.link_preview_description {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content
|
||||||
|
:global(.tgme_widget_message_link_preview):has(.link_preview_site_name) {
|
||||||
|
display: block;
|
||||||
|
background: var(--cell-background-color);
|
||||||
|
border-left: 3px solid var(--highlight-color);
|
||||||
|
padding: 6px;
|
||||||
|
padding-left: 10px;
|
||||||
|
border-radius: var(--box-border-radius);
|
||||||
|
|
||||||
|
.link_preview_title {
|
||||||
|
display: block;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: bolder;
|
||||||
|
line-height: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link_preview_description {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.8em;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :global(.tgme_widget_message_video) {
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :global(.tgme_widget_message_link_preview):has(.link_preview_image) {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
.link_preview_image {
|
||||||
|
aspect-ratio: 1200 / 630;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link_preview_site_name {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
bottom: var(--box-margin);
|
||||||
|
left: var(--box-margin);
|
||||||
|
padding-left: 4px;
|
||||||
|
padding-right: 4px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.66);
|
||||||
|
font-size: 14px;
|
||||||
|
color: #fff;
|
||||||
|
line-height: 1.5;
|
||||||
|
border-radius: var(--box-border-radius);
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: calc(100% - 28px);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link_preview_title,
|
||||||
|
.link_preview_description {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :global(blockquote) {
|
||||||
|
margin: 16px 0;
|
||||||
|
font-size: 0.8em;
|
||||||
|
background: var(--cell-background-color);
|
||||||
|
border-left: 3px solid var(--highlight-color);
|
||||||
|
padding: 6px;
|
||||||
|
padding-left: 10px;
|
||||||
|
border-radius: var(--box-border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :global(.tgme_widget_message_sticker) {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item :global(.content):has(.tgme_widget_message_user_photo) {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.tgme_widget_message_user_photo {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :global(.tgme_widget_message_voice) {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :global(.tgme_widget_message_poll_options) {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
.tgme_widget_message_poll_option_percent {
|
||||||
|
float: left;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content :global(.tgme_widget_message_location_wrap) {
|
||||||
|
display: block;
|
||||||
|
.tgme_widget_message_location {
|
||||||
|
padding-top: 50%;
|
||||||
|
background: no-repeat center;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
49
src/components/list.astro
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
import Layout from '../layouts/base.astro'
|
||||||
|
import Header from '../components/header.astro'
|
||||||
|
import Item from '../components/item.astro'
|
||||||
|
|
||||||
|
const { SITE_URL } = Astro.locals
|
||||||
|
const { channel, before = true, after = true } = Astro.props
|
||||||
|
const posts = channel.posts ?? []
|
||||||
|
|
||||||
|
const beforeCursor = posts[posts.length - 1]?.id
|
||||||
|
const afterCursor = posts[0]?.id
|
||||||
|
// const cursor = +Astro.params.cursor
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout channel={channel} id="main-container">
|
||||||
|
<slot name="header">
|
||||||
|
<Header channel={channel} />
|
||||||
|
</slot>
|
||||||
|
<div class="items">
|
||||||
|
{posts.map((post) => <Item post={post} />)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pages-container">
|
||||||
|
{
|
||||||
|
before && beforeCursor > 20 ? (
|
||||||
|
<a
|
||||||
|
href={`${SITE_URL}before/${beforeCursor}`}
|
||||||
|
title="Before"
|
||||||
|
class="page"
|
||||||
|
>
|
||||||
|
Before
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span class="page-placeholder"> </span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="pages-info"></div>
|
||||||
|
{
|
||||||
|
after && afterCursor ? (
|
||||||
|
<a href={`${SITE_URL}after/${afterCursor}`} title="After" class="page">
|
||||||
|
After
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span class="page-placeholder"> </span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
6
src/env.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/// <reference types="astro/client" />
|
||||||
|
declare namespace App {
|
||||||
|
interface Locals {
|
||||||
|
SITE_URL: string,
|
||||||
|
}
|
||||||
|
}
|
||||||
124
src/layouts/base.astro
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
---
|
||||||
|
import '../assets/normalize.css'
|
||||||
|
import '../assets/style.css'
|
||||||
|
import '../assets/global.css'
|
||||||
|
import { SEO } from 'astro-seo'
|
||||||
|
import { getEnv } from '../lib/env'
|
||||||
|
|
||||||
|
const { SITE_URL } = Astro.locals
|
||||||
|
const { channel } = Astro.props
|
||||||
|
|
||||||
|
const locale = getEnv(import.meta.env, Astro, 'LOCALE')
|
||||||
|
|
||||||
|
const seo = channel?.seo
|
||||||
|
const canonical = SITE_URL.startsWith('http')
|
||||||
|
? new URL(SITE_URL).origin + Astro.url.pathname
|
||||||
|
: Astro.url.origin + Astro.url.pathname
|
||||||
|
const origin = new URL(canonical).origin
|
||||||
|
const twitter = getEnv(import.meta.env, Astro, 'TWITTER')
|
||||||
|
|
||||||
|
const seoParams = {
|
||||||
|
title: seo?.title,
|
||||||
|
description: seo?.text ?? channel?.description,
|
||||||
|
canonical,
|
||||||
|
noindex: getEnv(import.meta.env, Astro, 'NOINDEX'),
|
||||||
|
nofollow: getEnv(import.meta.env, Astro, 'NOFOLLOW'),
|
||||||
|
openGraph: {
|
||||||
|
basic: {
|
||||||
|
type: 'website',
|
||||||
|
title: channel?.title ?? '',
|
||||||
|
url: canonical,
|
||||||
|
image: channel?.avatar ? channel.avatar : origin + '/favicon.ico',
|
||||||
|
},
|
||||||
|
optional: {
|
||||||
|
description: seo?.text ?? channel?.description,
|
||||||
|
locale: getEnv(import.meta.env, Astro, 'LOCALE'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
link: [{ rel: 'icon', href: '/favicon.svg' }],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const HEADER_INJECT = getEnv(import.meta.env, Astro, 'HEADER_INJECT')
|
||||||
|
const FOOTER_INJECT = getEnv(import.meta.env, Astro, 'FOOTER_INJECT')
|
||||||
|
---
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang={locale ?? 'en'}>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#f4f1ec" />
|
||||||
|
<link
|
||||||
|
rel="alternate"
|
||||||
|
type="application/rss+xml"
|
||||||
|
title={channel?.title}
|
||||||
|
href={origin + '/rss.xml'}
|
||||||
|
/>
|
||||||
|
<SEO
|
||||||
|
charset="utf-8"
|
||||||
|
titleTemplate={`%s | ${channel?.title}`}
|
||||||
|
titleDefault={[channel?.title, seoParams.description]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' - ')}
|
||||||
|
twitter={{
|
||||||
|
card: 'summary_large_image',
|
||||||
|
creator: twitter ? `@${twitter}` : undefined,
|
||||||
|
}}
|
||||||
|
{...seoParams}
|
||||||
|
/>
|
||||||
|
<Fragment set:html={HEADER_INJECT} />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="wrapper">
|
||||||
|
<div id="container">
|
||||||
|
<div id="main-container">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
<div id="aside-container">
|
||||||
|
<slot name="aside">
|
||||||
|
<div class="nav">
|
||||||
|
<div class="nav-item">
|
||||||
|
<a
|
||||||
|
href={SITE_URL}
|
||||||
|
title={channel?.title}
|
||||||
|
class={`nav-link current`}>Home</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
class="search-icon"
|
||||||
|
name="icon"
|
||||||
|
type="checkbox"
|
||||||
|
placeholder="Search"
|
||||||
|
/>
|
||||||
|
<form class="search-form" action="/search/result" method="get">
|
||||||
|
<input type="text" name="q" placeholder="Search" />
|
||||||
|
</form>
|
||||||
|
<div class="copyright-wrap">
|
||||||
|
Powered by
|
||||||
|
<a
|
||||||
|
href="https://github.com/ccbikai/BroadcastChannel"
|
||||||
|
title="BroadcastChannel"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
>
|
||||||
|
BroadcastChannel
|
||||||
|
</a> &
|
||||||
|
<a
|
||||||
|
href="https://github.com/Planetable/SiteTemplateSepia"
|
||||||
|
title="Sepia"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
>
|
||||||
|
Sepia
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Fragment set:html={FOOTER_INJECT} />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
158
src/lib/dayjs.js
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import dayjs from 'dayjs'
|
||||||
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
|
import updateLocale from 'dayjs/plugin/updateLocale'
|
||||||
|
import localizedFormat from 'dayjs/plugin/localizedFormat'
|
||||||
|
import utc from 'dayjs/plugin/utc'
|
||||||
|
import timezone from 'dayjs/plugin/timezone'
|
||||||
|
|
||||||
|
import 'dayjs/locale/af'
|
||||||
|
import 'dayjs/locale/am'
|
||||||
|
import 'dayjs/locale/ar-dz'
|
||||||
|
import 'dayjs/locale/ar-iq'
|
||||||
|
import 'dayjs/locale/ar-kw'
|
||||||
|
import 'dayjs/locale/ar-ly'
|
||||||
|
import 'dayjs/locale/ar-ma'
|
||||||
|
import 'dayjs/locale/ar-sa'
|
||||||
|
import 'dayjs/locale/ar-tn'
|
||||||
|
import 'dayjs/locale/ar'
|
||||||
|
import 'dayjs/locale/az'
|
||||||
|
import 'dayjs/locale/be'
|
||||||
|
import 'dayjs/locale/bg'
|
||||||
|
import 'dayjs/locale/bi'
|
||||||
|
import 'dayjs/locale/bm'
|
||||||
|
import 'dayjs/locale/bn-bd'
|
||||||
|
import 'dayjs/locale/bn'
|
||||||
|
import 'dayjs/locale/bo'
|
||||||
|
import 'dayjs/locale/br'
|
||||||
|
import 'dayjs/locale/bs'
|
||||||
|
import 'dayjs/locale/ca'
|
||||||
|
import 'dayjs/locale/cs'
|
||||||
|
import 'dayjs/locale/cv'
|
||||||
|
import 'dayjs/locale/cy'
|
||||||
|
import 'dayjs/locale/da'
|
||||||
|
import 'dayjs/locale/de-at'
|
||||||
|
import 'dayjs/locale/de-ch'
|
||||||
|
import 'dayjs/locale/de'
|
||||||
|
import 'dayjs/locale/dv'
|
||||||
|
import 'dayjs/locale/el'
|
||||||
|
import 'dayjs/locale/en-au'
|
||||||
|
import 'dayjs/locale/en-ca'
|
||||||
|
import 'dayjs/locale/en-gb'
|
||||||
|
import 'dayjs/locale/en-ie'
|
||||||
|
import 'dayjs/locale/en-il'
|
||||||
|
import 'dayjs/locale/en-in'
|
||||||
|
import 'dayjs/locale/en-nz'
|
||||||
|
import 'dayjs/locale/en-sg'
|
||||||
|
import 'dayjs/locale/en-tt'
|
||||||
|
import 'dayjs/locale/en'
|
||||||
|
import 'dayjs/locale/eo'
|
||||||
|
import 'dayjs/locale/es-do'
|
||||||
|
import 'dayjs/locale/es-mx'
|
||||||
|
import 'dayjs/locale/es-pr'
|
||||||
|
import 'dayjs/locale/es-us'
|
||||||
|
import 'dayjs/locale/es'
|
||||||
|
import 'dayjs/locale/et'
|
||||||
|
import 'dayjs/locale/eu'
|
||||||
|
import 'dayjs/locale/fa'
|
||||||
|
import 'dayjs/locale/fi'
|
||||||
|
import 'dayjs/locale/fo'
|
||||||
|
import 'dayjs/locale/fr-ca'
|
||||||
|
import 'dayjs/locale/fr-ch'
|
||||||
|
import 'dayjs/locale/fr'
|
||||||
|
import 'dayjs/locale/fy'
|
||||||
|
import 'dayjs/locale/ga'
|
||||||
|
import 'dayjs/locale/gd'
|
||||||
|
import 'dayjs/locale/gl'
|
||||||
|
import 'dayjs/locale/gom-latn'
|
||||||
|
import 'dayjs/locale/gu'
|
||||||
|
import 'dayjs/locale/he'
|
||||||
|
import 'dayjs/locale/hi'
|
||||||
|
import 'dayjs/locale/hr'
|
||||||
|
import 'dayjs/locale/ht'
|
||||||
|
import 'dayjs/locale/hu'
|
||||||
|
import 'dayjs/locale/hy-am'
|
||||||
|
import 'dayjs/locale/id'
|
||||||
|
import 'dayjs/locale/is'
|
||||||
|
import 'dayjs/locale/it-ch'
|
||||||
|
import 'dayjs/locale/it'
|
||||||
|
import 'dayjs/locale/ja'
|
||||||
|
import 'dayjs/locale/jv'
|
||||||
|
import 'dayjs/locale/ka'
|
||||||
|
import 'dayjs/locale/kk'
|
||||||
|
import 'dayjs/locale/km'
|
||||||
|
import 'dayjs/locale/kn'
|
||||||
|
import 'dayjs/locale/ko'
|
||||||
|
import 'dayjs/locale/ku'
|
||||||
|
import 'dayjs/locale/ky'
|
||||||
|
import 'dayjs/locale/lb'
|
||||||
|
import 'dayjs/locale/lo'
|
||||||
|
import 'dayjs/locale/lt'
|
||||||
|
import 'dayjs/locale/lv'
|
||||||
|
import 'dayjs/locale/me'
|
||||||
|
import 'dayjs/locale/mi'
|
||||||
|
import 'dayjs/locale/mk'
|
||||||
|
import 'dayjs/locale/ml'
|
||||||
|
import 'dayjs/locale/mn'
|
||||||
|
import 'dayjs/locale/mr'
|
||||||
|
import 'dayjs/locale/ms-my'
|
||||||
|
import 'dayjs/locale/ms'
|
||||||
|
import 'dayjs/locale/mt'
|
||||||
|
import 'dayjs/locale/my'
|
||||||
|
import 'dayjs/locale/nb'
|
||||||
|
import 'dayjs/locale/ne'
|
||||||
|
import 'dayjs/locale/nl-be'
|
||||||
|
import 'dayjs/locale/nl'
|
||||||
|
import 'dayjs/locale/nn'
|
||||||
|
import 'dayjs/locale/oc-lnc'
|
||||||
|
import 'dayjs/locale/pa-in'
|
||||||
|
import 'dayjs/locale/pl'
|
||||||
|
import 'dayjs/locale/pt-br'
|
||||||
|
import 'dayjs/locale/pt'
|
||||||
|
import 'dayjs/locale/rn'
|
||||||
|
import 'dayjs/locale/ro'
|
||||||
|
import 'dayjs/locale/ru'
|
||||||
|
import 'dayjs/locale/rw'
|
||||||
|
import 'dayjs/locale/sd'
|
||||||
|
import 'dayjs/locale/se'
|
||||||
|
import 'dayjs/locale/si'
|
||||||
|
import 'dayjs/locale/sk'
|
||||||
|
import 'dayjs/locale/sl'
|
||||||
|
import 'dayjs/locale/sq'
|
||||||
|
import 'dayjs/locale/sr-cyrl'
|
||||||
|
import 'dayjs/locale/sr'
|
||||||
|
import 'dayjs/locale/ss'
|
||||||
|
import 'dayjs/locale/sv-fi'
|
||||||
|
import 'dayjs/locale/sv'
|
||||||
|
import 'dayjs/locale/sw'
|
||||||
|
import 'dayjs/locale/ta'
|
||||||
|
import 'dayjs/locale/te'
|
||||||
|
import 'dayjs/locale/tet'
|
||||||
|
import 'dayjs/locale/tg'
|
||||||
|
import 'dayjs/locale/th'
|
||||||
|
import 'dayjs/locale/tk'
|
||||||
|
import 'dayjs/locale/tl-ph'
|
||||||
|
import 'dayjs/locale/tlh'
|
||||||
|
import 'dayjs/locale/tr'
|
||||||
|
import 'dayjs/locale/tzl'
|
||||||
|
import 'dayjs/locale/tzm-latn'
|
||||||
|
import 'dayjs/locale/tzm'
|
||||||
|
import 'dayjs/locale/ug-cn'
|
||||||
|
import 'dayjs/locale/uk'
|
||||||
|
import 'dayjs/locale/ur'
|
||||||
|
import 'dayjs/locale/uz-latn'
|
||||||
|
import 'dayjs/locale/uz'
|
||||||
|
import 'dayjs/locale/vi'
|
||||||
|
import 'dayjs/locale/x-pseudo'
|
||||||
|
import 'dayjs/locale/yo'
|
||||||
|
import 'dayjs/locale/zh-cn'
|
||||||
|
import 'dayjs/locale/zh-hk'
|
||||||
|
import 'dayjs/locale/zh-tw'
|
||||||
|
import 'dayjs/locale/zh'
|
||||||
|
|
||||||
|
dayjs.extend(updateLocale)
|
||||||
|
dayjs.extend(relativeTime)
|
||||||
|
dayjs.extend(localizedFormat)
|
||||||
|
dayjs.extend(utc)
|
||||||
|
dayjs.extend(timezone)
|
||||||
|
|
||||||
|
export default dayjs
|
||||||
3
src/lib/env.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function getEnv(env, Astro, name) {
|
||||||
|
return env[name] ?? Astro.locals?.runtime?.env?.[name]
|
||||||
|
}
|
||||||
146
src/lib/telegram/index.js
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import { $fetch } from 'ofetch'
|
||||||
|
import * as cheerio from 'cheerio'
|
||||||
|
import { LRUCache } from 'lru-cache'
|
||||||
|
import { getEnv } from '../env'
|
||||||
|
|
||||||
|
const cache = new LRUCache({
|
||||||
|
ttl: 1000 * 60 * 5, // 5 minutes
|
||||||
|
maxSize: 50 * 1024 * 1024, // 50MB
|
||||||
|
sizeCalculation: (item) => {
|
||||||
|
return JSON.stringify(item).length
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
function getImages($, item, { staticProxy, id, index, title }) {
|
||||||
|
return $(item).find('.tgme_widget_message_photo_wrap')?.map((_index, photo) => {
|
||||||
|
const url = $(photo).attr('style').match(/url\(["'](.*?)["']/)?.[1]
|
||||||
|
const popoverId = `modal-${id}-${_index}`
|
||||||
|
return `
|
||||||
|
<button class="image-preview-button image-preview-wrap" popovertarget="${popoverId}" popovertargetaction="show">
|
||||||
|
<img src="${staticProxy + url}" alt="${title}" loading="${index > 15 ? 'eager' : 'lazy'}" />
|
||||||
|
</button>
|
||||||
|
<button class="image-preview-button modal" id="${popoverId}" popovertarget="${popoverId}" popovertargetaction="hide" popover>
|
||||||
|
<img class="modal-img" src="${staticProxy + url}" alt="${title}" loading="${index > 15 ? 'eager' : 'lazy'}" />
|
||||||
|
</button>
|
||||||
|
`
|
||||||
|
})?.get()?.join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVideo($, item, { staticProxy, index }) {
|
||||||
|
const video = $(item).find('.tgme_widget_message_video_wrap video')
|
||||||
|
video?.attr('src', staticProxy + video?.attr('src'))?.attr('controls', true)?.attr('preload', index > 15 ? 'auto' : 'metadata')
|
||||||
|
return $.html(video)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLinkPreview($, item, { staticProxy, index }) {
|
||||||
|
const link = $(item).find('.tgme_widget_message_link_preview')
|
||||||
|
const title = $(item).find('.link_preview_title')?.text() || $(item).find('.link_preview_site_name')?.text()
|
||||||
|
const description = $(item).find('.link_preview_description')?.text()
|
||||||
|
|
||||||
|
link?.attr('target', '_blank').attr('rel', 'noopener').attr('title', description)
|
||||||
|
|
||||||
|
const image = $(item).find('.link_preview_image')
|
||||||
|
const src = image?.attr('style')?.match(/url\(["'](.*?)["']/i)?.[1]
|
||||||
|
const imageSrc = src ? staticProxy + src : ''
|
||||||
|
image?.replaceWith(`<img class="link_preview_image" alt="${title}" src="${imageSrc}" loading="${index > 15 ? 'eager' : 'lazy'}" />`)
|
||||||
|
return $.html(link)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPost($, item, { channel, staticProxy, index = 0 }) {
|
||||||
|
item = item ? $(item).find('.tgme_widget_message') : $('.tgme_widget_message')
|
||||||
|
const content = $(item).find('.tgme_widget_message_bubble > .tgme_widget_message_text')
|
||||||
|
const title = content?.text()?.match(/[^。\n]*(?=[。\n]|http)/g)?.[0] ?? content?.text() ?? ''
|
||||||
|
const id = $(item).attr('data-post')?.replace(`${channel}/`, '')
|
||||||
|
|
||||||
|
$(content).find('a').each((_index, a) => {
|
||||||
|
$(a).attr('title', $(a).text())
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
type: $(item).attr('class')?.includes('service_message') ? 'service' : 'text',
|
||||||
|
datetime: $(item).find('.tgme_widget_message_date time')?.attr('datetime'),
|
||||||
|
tags: $(item).find('a[href^="?q"]')?.map((_index, item) => $(item).text()?.replace('#', ''))?.get(),
|
||||||
|
text: content?.text(),
|
||||||
|
content: [
|
||||||
|
getImages($, item, { staticProxy, id, index, title }),
|
||||||
|
getVideo($, item, { staticProxy, id, index, title }),
|
||||||
|
content?.html(),
|
||||||
|
// $(item).find('.tgme_widget_message_sticker_wrap')?.html(),
|
||||||
|
$(item).find('.tgme_widget_message_poll')?.html(),
|
||||||
|
$.html($(item).find('.tgme_widget_message_document_wrap')),
|
||||||
|
$.html($(item).find('.tgme_widget_message_roundvideo')?.attr('controls', true)),
|
||||||
|
$.html($(item).find('.tgme_widget_message_voice')?.attr('controls', true)),
|
||||||
|
$.html($(item).find('.tgme_widget_message_location_wrap')),
|
||||||
|
$.html($(item).find('.tgme_widget_message_reply .tgme_widget_message_text')?.wrapInner('<blockquote></blockquote>')),
|
||||||
|
getLinkPreview($, item, { staticProxy, index }),
|
||||||
|
].filter(Boolean).join('').replace(/(url\(["'])((https?:)?\/\/)/g, (match, p1, p2, _p3) => {
|
||||||
|
if (p2 === '//') {
|
||||||
|
p2 = 'https://'
|
||||||
|
}
|
||||||
|
if (p2?.startsWith('t.me')) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return `${p1}${staticProxy}${p2}`
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const unnessaryHeaders = ['host', 'cookie', 'origin', 'referer']
|
||||||
|
|
||||||
|
export async function getChannelInfo(Astro, { before = '', after = '', q = '', id = '' } = {}) {
|
||||||
|
const cacheKey = JSON.stringify({ before, after, q, id })
|
||||||
|
const cachedResult = cache.get(cacheKey)
|
||||||
|
|
||||||
|
if (cachedResult) {
|
||||||
|
console.info('Macth Cache', { before, after, q, id })
|
||||||
|
return JSON.parse(JSON.stringify(cachedResult))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Where t.me can also be telegram.me, telegram.dog
|
||||||
|
const host = getEnv(import.meta.env, Astro, 'HOST') ?? 't.me'
|
||||||
|
const channel = getEnv(import.meta.env, Astro, 'CHANNEL')
|
||||||
|
const staticProxy = getEnv(import.meta.env, Astro, 'STATIC_PROXY') ?? '/static/'
|
||||||
|
|
||||||
|
const url = id ? `https://${host}/${channel}/${id}?embed=1&mode=tme` : `https://${host}/s/${channel}`
|
||||||
|
const headers = Object.fromEntries(Astro.request.headers)
|
||||||
|
|
||||||
|
Object.keys(headers).forEach((key) => {
|
||||||
|
if (unnessaryHeaders.includes(key)) {
|
||||||
|
delete headers[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.info('Fetching', url, { before, after, q, id })
|
||||||
|
const html = await $fetch(url, {
|
||||||
|
headers,
|
||||||
|
query: {
|
||||||
|
before: before || undefined,
|
||||||
|
after: after || undefined,
|
||||||
|
q: q || undefined,
|
||||||
|
},
|
||||||
|
retry: 3,
|
||||||
|
retryDelay: 100,
|
||||||
|
})
|
||||||
|
|
||||||
|
const $ = cheerio.load(html, {}, false)
|
||||||
|
if (id) {
|
||||||
|
const post = getPost($, null, { channel, staticProxy })
|
||||||
|
cache.set(cacheKey, post)
|
||||||
|
return post
|
||||||
|
}
|
||||||
|
const posts = $('.tgme_channel_history .tgme_widget_message_wrap')?.map((index, item) => {
|
||||||
|
return getPost($, item, { channel, staticProxy, index })
|
||||||
|
})?.get()?.reverse().filter(post => ['text'].includes(post.type) && post.id && post.content)
|
||||||
|
|
||||||
|
const channelInfo = {
|
||||||
|
posts,
|
||||||
|
title: $('.tgme_channel_info_header_title')?.text(),
|
||||||
|
description: $('.tgme_channel_info_description')?.text(),
|
||||||
|
avatar: $('.tgme_page_photo_image img')?.attr('src'),
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.set(cacheKey, channelInfo)
|
||||||
|
return channelInfo
|
||||||
|
}
|
||||||
4
src/middleware.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export function onRequest(context, next) {
|
||||||
|
context.locals.SITE_URL = `${import.meta.env.SITE ?? ''}${import.meta.env.BASE_URL}`
|
||||||
|
return next()
|
||||||
|
};
|
||||||
12
src/pages/after/[cursor].astro
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
import List from '../../components/list.astro'
|
||||||
|
import { getChannelInfo } from '../../lib/telegram'
|
||||||
|
|
||||||
|
const channel = await getChannelInfo(Astro, {
|
||||||
|
after: Astro.params.cursor,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const prerender = false
|
||||||
|
---
|
||||||
|
|
||||||
|
<List channel={channel} />
|
||||||
12
src/pages/before/[cursor].astro
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
import List from '../../components/list.astro'
|
||||||
|
import { getChannelInfo } from '../../lib/telegram'
|
||||||
|
|
||||||
|
const channel = await getChannelInfo(Astro, {
|
||||||
|
before: Astro.params.cursor,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const prerender = false
|
||||||
|
---
|
||||||
|
|
||||||
|
<List channel={channel} />
|
||||||
12
src/pages/index.astro
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
import List from '../components/list.astro'
|
||||||
|
import { getChannelInfo } from '../lib/telegram'
|
||||||
|
|
||||||
|
const channel = await getChannelInfo(Astro, {
|
||||||
|
q: Astro.url.searchParams.get('q') || '',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const prerender = false
|
||||||
|
---
|
||||||
|
|
||||||
|
<List channel={channel} after={false} />
|
||||||
26
src/pages/posts/[id].astro
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
import List from '../../components/list.astro'
|
||||||
|
import { getChannelInfo } from '../../lib/telegram'
|
||||||
|
|
||||||
|
const { SITE_URL } = Astro.locals
|
||||||
|
|
||||||
|
const channel = await getChannelInfo(Astro)
|
||||||
|
|
||||||
|
const post = await getChannelInfo(Astro, {
|
||||||
|
id: Astro.params.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
channel.posts = [post]
|
||||||
|
channel.seo = post
|
||||||
|
|
||||||
|
export const prerender = false
|
||||||
|
---
|
||||||
|
|
||||||
|
<List channel={channel} before={false} after={false}>
|
||||||
|
<div slot="header" id="breadcrumb">
|
||||||
|
<img src={channel.avatar} class="breadcrumb-avatar" />
|
||||||
|
<div class="breadcrumb-title">
|
||||||
|
<a href={SITE_URL} class="site-title">{channel.title}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</List>
|
||||||
28
src/pages/rss.json.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { getChannelInfo } from '../lib/telegram'
|
||||||
|
|
||||||
|
export const prerender = false
|
||||||
|
|
||||||
|
export async function GET(Astro) {
|
||||||
|
const request = Astro.request
|
||||||
|
const { SITE_URL } = Astro.locals
|
||||||
|
const channel = await getChannelInfo(Astro)
|
||||||
|
const posts = channel.posts || []
|
||||||
|
|
||||||
|
const url = new URL(request.url)
|
||||||
|
url.pathname = SITE_URL
|
||||||
|
|
||||||
|
return Response.json({
|
||||||
|
version: 'https://jsonfeed.org/version/1.1',
|
||||||
|
title: channel.title,
|
||||||
|
description: channel.description,
|
||||||
|
home_page_url: url.toString(),
|
||||||
|
items: posts.map(item => ({
|
||||||
|
url: `${url.toString()}posts/${item.id}`,
|
||||||
|
title: item.title,
|
||||||
|
description: item.description,
|
||||||
|
date_published: new Date(item.datetime),
|
||||||
|
tags: item.tags,
|
||||||
|
content_html: item.content,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
}
|
||||||
28
src/pages/rss.xml.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import rss from '@astrojs/rss'
|
||||||
|
|
||||||
|
import { getChannelInfo } from '../lib/telegram'
|
||||||
|
|
||||||
|
export const prerender = false
|
||||||
|
|
||||||
|
export async function GET(Astro) {
|
||||||
|
const request = Astro.request
|
||||||
|
const { SITE_URL } = Astro.locals
|
||||||
|
const channel = await getChannelInfo(Astro)
|
||||||
|
const posts = channel.posts || []
|
||||||
|
|
||||||
|
const url = new URL(request.url)
|
||||||
|
url.pathname = SITE_URL
|
||||||
|
|
||||||
|
return rss({
|
||||||
|
title: channel.title,
|
||||||
|
description: channel.description,
|
||||||
|
site: url.origin,
|
||||||
|
items: posts.map(item => ({
|
||||||
|
link: `posts/${item.id}`,
|
||||||
|
title: item.title,
|
||||||
|
description: item.description,
|
||||||
|
pubDate: new Date(item.datetime),
|
||||||
|
content: item.content,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
}
|
||||||
12
src/pages/search/[q].astro
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
import List from '../../components/list.astro'
|
||||||
|
import { getChannelInfo } from '../../lib/telegram'
|
||||||
|
|
||||||
|
const channel = await getChannelInfo(Astro, {
|
||||||
|
q: Astro.url.searchParams.get('q') || Astro.params.q,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const prerender = false
|
||||||
|
---
|
||||||
|
|
||||||
|
<List channel={channel} before={false} after={false} />
|
||||||
18
src/pages/static/[...url].js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
const targetWhitelist = [
|
||||||
|
't.me',
|
||||||
|
'telegram.org',
|
||||||
|
'telegram.me',
|
||||||
|
'telegram.dog',
|
||||||
|
'cdn-telegram.org',
|
||||||
|
]
|
||||||
|
|
||||||
|
export const prerender = false
|
||||||
|
|
||||||
|
export async function GET(Astro) {
|
||||||
|
const { request, params, url } = Astro
|
||||||
|
const target = new URL(params.url + url.search)
|
||||||
|
if (!targetWhitelist.some(host => target.host.endsWith(host))) {
|
||||||
|
return Astro.redirect(target.toString(), 302)
|
||||||
|
}
|
||||||
|
return fetch(target.toString(), request)
|
||||||
|
}
|
||||||
3
tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/base"
|
||||||
|
}
|
||||||