feat: init

This commit is contained in:
ccbikai
2024-08-04 20:42:51 +08:00
commit c39ab2d528
45 changed files with 12703 additions and 0 deletions

12
.editorconfig Normal file
View 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
View 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
View 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
View File

@@ -0,0 +1 @@
v20

4
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}

11
.vscode/launch.json vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

6
postcss.config.cjs Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: [
require('autoprefixer'),
require('cssnano'),
],
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

1
public/favicon.svg Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 882 B

1
src/assets/telegram.svg Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

135
src/components/header.astro Normal file
View 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
View 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
View 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">&nbsp;</span>
)
}
<div class="pages-info"></div>
{
after && afterCursor ? (
<a href={`${SITE_URL}after/${afterCursor}`} title="After" class="page">
After
</a>
) : (
<span class="page-placeholder">&nbsp;</span>
)
}
</div>
</Layout>

6
src/env.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
/// <reference types="astro/client" />
declare namespace App {
interface Locals {
SITE_URL: string,
}
}

124
src/layouts/base.astro Normal file
View 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
View 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
View 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
View 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
View 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()
};

View 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} />

View 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
View 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} />

View 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
View 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
View 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,
})),
})
}

View 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} />

View 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
View File

@@ -0,0 +1,3 @@
{
"extends": "astro/tsconfigs/base"
}