初步汉化

This commit is contained in:
blue-cape 2025-03-10 17:05:03 +08:00
commit 9ff9f36b8d
111 changed files with 10385 additions and 0 deletions

8
.dockerignore Normal file
View File

@ -0,0 +1,8 @@
# Ignore Astro files
*.astro
# Ignore node_modules directory
node_modules/
# Ignore build output
dist/

124
.github/CODE_OF_CONDUCT.md vendored Normal file
View File

@ -0,0 +1,124 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or
advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at [satnaingdev@gmail.com](satnaingdev@gmail.com).
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at [this link](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html).
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the [FAQ](https://www.contributor-covenant.org/faq). [Translations](https://www.contributor-covenant.org/translations) are also available.

55
.github/CONTRIBUTING.md vendored Normal file
View File

@ -0,0 +1,55 @@
# How to contribute to AstroPaper
Thank you for your interest in contributing to **AstroPaper**! We appreciate every contribution, whether you're fixing a typo, improving documentation, or adding a new feature.
## Types of Contributions
There are several ways to contribute to **AstroPaper**, and every contribution counts\_ whether it's a PR for a major feature or a small fix.
You can also contribute by leaving review comments on PRs, adding ideas to existing GitHub Issues and Discussions, or helping others by answering questions in GitHub Discussions.
Heres a summary of the different ways you can contribute:
- [Opening a new issue](#open-a-new-issue)
- [Submitting PRs](#feature-requests)
- [Solving an existing issue](#solving-an-issue)
- [Making changes to a blog post](#making-changes-to-a-blog-post)
- [Helping others by answering issues/discussions](#helping-with-github-issuesdiscussions)
- [Reviewing existing PRs](#reviewing-existing-prs)
- [Starting a discussion](#starting-a-discussion)
## Open a new Issue
If you find a bug or problem, first check whether a similar issue already exists. If you dont find any open issue that addresses the bug/problem youre facing, feel free to [open a new issue](https://github.com/satnaing/astro-paper/issues/new/choose).
## Feature Requests
If you have an idea for a new feature or enhancement that could improve AstroPaper, wed love to hear it! Before submitting a new feature request, please:
1. **Check existing discussions/issues**: Review the [Discussions](https://github.com/satnaing/astro-paper/discussions) or [Issues](https://github.com/satnaing/astro-paper/issues) to see if the feature has already been requested or discussed. You can contribute by adding your thoughts or upvoting existing requests.
2. **Open a new issue**: If you dont find an existing discussion, you can open a new issue using the [Feature Request Template](https://github.com/satnaing/astro-paper/issues/new?assignees=&labels=enhancement&projects=&template=%E2%9C%A8-feature-request.md&title=%5BFeature+Request%5D%3A+). Be as detailed as possible, describing the problem this feature would solve and how it would benefit AstroPaper users.
3. **Discuss first**: If youre unsure whether your idea is feasible or fits the projects goals, feel free to [start a GitHub Discussion](https://github.com/satnaing/astro-paper/discussions/new/choose) to gather feedback from the community.
## Making PRs (Pull Requests)
### Solving an Issue
Browse through the existing issues to find one that interests you. You can use labels to filter the issues. See the [Label](https://github.com/satnaing/astro-paper/labels) section for more information.
### Making Changes to a Blog Post
For small changes like typos, syntax fixes, or broken links, click the "Suggest Changes" link below the title of any blog post. This will take you to the .md file, where you can make your changes and submit a pull request for review. For more significant changes to a blog post, its recommended to open a new issue or discussion first.
## Helping with GitHub Issues/Discussions
GitHub Discussions and Issues are great places to help others. Whether you're a long-time user of AstroPaper or just have experience with a specific problem, we encourage you to answer questions or solve issues when possible.
## Reviewing Existing PRs
You can help by reviewing and providing feedback on open PRs. Different perspectives can be very helpful.
Since AstroPaper doesnt currently have automated testing, its especially useful if you can do manual testing on open PRs and provide feedback.
## Starting a Discussion
If youre unsure whether your issue warrants a fix or if you just want to share ideas and get feedback, feel free to [start a GitHub discussion](https://github.com/satnaing/astro-paper/discussions/new/choose). Its a great way to engage with the community.

2
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,2 @@
github: [satnaing]
buy_me_a_coffee: satnaing

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: AstroPaper Discussions
url: https://github.com/satnaing/astro-paper/discussions
about: Please ask and answer questions here.

View File

@ -0,0 +1,19 @@
---
name: "✨ Feature Request"
about: Suggest an idea for improving AstroPaper
title: "[Feature Request]: "
labels: enhancement
assignees: ""
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -0,0 +1,27 @@
---
name: "\U0001F41E Bug report"
about: Report a bug or unexpected behavior in AstroPaper
title: "[BUG]: "
labels: bug
assignees: ""
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Additional context**
Add any other context about the problem here.

View File

@ -0,0 +1,16 @@
---
name: "\U0001F4DD Documentation Improvement"
about: Propose updates or improvements to the documentation/blog posts
title: "[Docs]: "
labels: documentation
assignees: ""
---
**Describe the Issue**
A clear and concise description of the documentation issue or improvement.
**Proposed Changes**
Describe what changes should be made and why they would improve the documentation.
**Additional Context**
Add any other context or screenshots about the documentation request here.

30
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,30 @@
## Description
<!-- A clear and concise description of what the pull request does. Include any relevant motivation and background. -->
## Types of changes
<!-- What types of changes does your code introduce to AstroPaper? Put an `x` in the boxes that apply -->
- [ ] Bug Fix (non-breaking change which fixes an issue)
- [ ] New Feature (non-breaking change which adds functionality)
- [ ] Documentation Update (if none of the other choices apply)
- [ ] Others (any other types not listed above)
## Checklist
<!-- Please follow this checklist and put an x in each of the boxes, like this: [x]. You can also fill these out after creating the PR. This is simply a reminder of what we are going to look for before merging your code. -->
- [ ] I have read the [Contributing Guide](https://github.com/satnaing/astro-paper/blob/main/.github/CONTRIBUTING.md)
- [ ] I have added the necessary documentation (if appropriate)
- [ ] Breaking Change (fix or feature that would cause existing functionality to not work as expected)
## Further comments
<!-- If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... -->
## Related Issue
<!-- If this PR is related to an existing issue, link to it here. -->
Closes: #<!-- Issue number, if applicable -->

47
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,47 @@
name: CI
on:
pull_request:
types:
- opened
- edited
- synchronize
- reopened
workflow_call:
jobs:
build:
name: Code standards & build
runs-on: ubuntu-latest
timeout-minutes: 3
strategy:
matrix:
node-version: [20]
steps:
- name: "☁️ Checkout repository"
uses: actions/checkout@v4
- name: "📦 Install pnpm"
uses: pnpm/action-setup@v4
with:
version: 10
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: "pnpm"
- name: "📦 Install dependencies"
run: pnpm install
- name: "🔎 Lint code"
run: pnpm run lint
- name: "📝 Checking code format"
run: pnpm run format:check
- name: "🚀 Build the project"
run: pnpm run build

28
.gitignore vendored Normal file
View File

@ -0,0 +1,28 @@
# 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/
# pagefind
public/pagefind

14
.prettierignore Normal file
View File

@ -0,0 +1,14 @@
# Ignore everything
/*
# Except these files & folders
!/src
!/public
!/.github
!tsconfig.json
!astro.config.ts
!.prettierrc.mjs
!package.json
!.prettierrc
!eslint.config.js
!README.md

22
.prettierrc.mjs Normal file
View File

@ -0,0 +1,22 @@
/** @type {import("prettier").Config} */
export default {
arrowParens: "avoid",
semi: true,
tabWidth: 2,
printWidth: 80,
singleQuote: false,
jsxSingleQuote: false,
trailingComma: "es5",
bracketSpacing: true,
endOfLine: "lf",
plugins: ["prettier-plugin-astro", "prettier-plugin-tailwindcss"],
tailwindStylesheet: "./src/styles/global.css",
overrides: [
{
files: "*.astro",
options: {
parser: "astro",
},
},
],
};

34
.vscode/astro-paper.code-snippets vendored Normal file
View File

@ -0,0 +1,34 @@
{
"Frontmatter": {
"scope": "markdown",
"prefix": "frontmatter",
"body": [
"---",
"author: $1",
"pubDatetime: $CURRENT_YEAR-$CURRENT_MONTH-${CURRENT_DATE}T$CURRENT_HOUR:$CURRENT_MINUTE:$CURRENT_SECOND.000$CURRENT_TIMEZONE_OFFSET",
"modDatetime: $3",
"title: $4",
"featured: ${5|false,true|}",
"draft: ${6|true,false|}",
"tags:",
" - $7",
"description: $8",
"---",
],
"description": "Adds the frontmatter block for the AstroPaper Blog post"
},
"Blog Template": {
"scope": "markdown",
"prefix": "template",
"body": [
"${1:frontmatter}",
"",
"${2: Introductory Sentence}",
"",
"## Table of contents",
"",
"## ${3: heading 1}",
],
"description": "Adds the template for the AstroPaper Blog post. You will need to trigger the snippet modal on the 'frontmatter' line to insert the other snipper."
}
}

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"
}
]
}

View File

@ -0,0 +1,159 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="theme--agnostic" fill="none" width="1000" height="330">
<style>
.gauge-base {
opacity: 0.1
}
.gauge-arc {
fill: none;
animation-delay: 250ms;
stroke-linecap: round;
transform: rotate(-90deg);
transform-origin: 100px 60px;
animation: load-gauge 1s ease forwards
}
.guage-text {
font-size: 40px;
font-family: monospace;
text-align: center
}
.guage-red {
color: #ff4e42;
fill: #ff4e42;
stroke: #ff4e42
}
.guage-orange {
color: #ffa400;
fill: #ffa400;
stroke: #ffa400
}
.guage-green {
color: #0cce6b;
fill: #0cce6b;
stroke: #0cce6b
}
.theme--agnostic .guage-undefined {
color: #5c5c5c;
fill: #5c5c5c;
stroke: #5c5c5c
}
.theme--light .guage-undefined {
color: #1e1e1e;
fill: #1e1e1e;
stroke: #1e1e1e
}
.theme--dark .guage-undefined {
color: #f5f5f5;
fill: #f5f5f5;
stroke: #f5f5f5
}
.guage-title {
stroke: none;
font-size: 26px;
line-height: 26px;
font-family: Roboto, Halvetica, Arial, sans-serif
}
.metric.guage-title {
font-family: 'Courier New', Courier, monospace
}
.theme--agnostic .guage-title {
color: #737373;
fill: #737373
}
.theme--light .guage-title {
color: #212121;
fill: #212121
}
.theme--dark .guage-title {
color: #f5f5f5;
fill: #f5f5f5
}
@keyframes load-gauge {
from {
stroke-dasharray: 0 352.858
}
}
.lh-gauge--pwa__disc {
fill: #e0e0e0
}
.lh-gauge--pwa__logo {
position: relative;
fill: #b0b0b0
}
.lh-gauge--pwa__invisible {
display: none
}
.lh-gauge--pwa__visible {
display: inline
}
.guage-invisible {
display: none
}
.lh-gauge--pwa__logo--primary-color {
fill: #304ffe
}
.theme--agnostic .lh-gauge--pwa__logo--secondary-color {
fill: #787878
}
.theme--light .lh-gauge--pwa__logo--secondary-color {
fill: #3d3d3d
}
.theme--dark .lh-gauge--pwa__logo--secondary-color {
fill: #d8b6b6
}
.theme--light #svg_2 {
stroke: #00000022
}
.theme--agnostic #svg_2 {
stroke: #616161
}
.theme--light #svg_2 {
stroke: #00000022
}
.theme--dark #svg_2 {
stroke: #f5f5f566
}
</style>
<svg class="guage-div guage-perf guage-green" viewBox="0 0 200 200" width="200" height="200" x="100" y="0">
<circle class="gauge-base" r="56" cx="100" cy="60" stroke-width="8"/>
<circle class="gauge-arc guage-arc-1" r="56" cx="100" cy="60" stroke-width="8" style="stroke-dasharray: 351.858, 351.858;"/>
<text class="guage-text" x="100px" y="60px" alignment-baseline="central" dominant-baseline="central" text-anchor="middle">100</text>
<text class="guage-title" x="100px" y="160px" alignment-baseline="central" dominant-baseline="central" text-anchor="middle">Performance</text>
</svg>,<svg class="guage-div guage-perf guage-green" viewBox="0 0 200 200" width="200" height="200" x="300" y="0">
<circle class="gauge-base" r="56" cx="100" cy="60" stroke-width="8"/>
<circle class="gauge-arc guage-arc-1" r="56" cx="100" cy="60" stroke-width="8" style="stroke-dasharray: 351.858, 351.858;"/>
<text class="guage-text" x="100px" y="60px" alignment-baseline="central" dominant-baseline="central" text-anchor="middle">100</text>
<text class="guage-title" x="100px" y="160px" alignment-baseline="central" dominant-baseline="central" text-anchor="middle">Accessibility</text>
</svg>,<svg class="guage-div guage-perf guage-green" viewBox="0 0 200 200" width="200" height="200" x="500" y="0">
<circle class="gauge-base" r="56" cx="100" cy="60" stroke-width="8"/>
<circle class="gauge-arc guage-arc-1" r="56" cx="100" cy="60" stroke-width="8" style="stroke-dasharray: 351.858, 351.858;"/>
<text class="guage-text" x="100px" y="60px" alignment-baseline="central" dominant-baseline="central" text-anchor="middle">100</text>
<text class="guage-title" x="100px" y="160px" alignment-baseline="central" dominant-baseline="central" text-anchor="middle">Best Practices</text>
</svg>,<svg class="guage-div guage-perf guage-green" viewBox="0 0 200 200" width="200" height="200" x="700" y="0">
<circle class="gauge-base" r="56" cx="100" cy="60" stroke-width="8"/>
<circle class="gauge-arc guage-arc-1" r="56" cx="100" cy="60" stroke-width="8" style="stroke-dasharray: 351.858, 351.858;"/>
<text class="guage-text" x="100px" y="60px" alignment-baseline="central" dominant-baseline="central" text-anchor="middle">100</text>
<text class="guage-title" x="100px" y="160px" alignment-baseline="central" dominant-baseline="central" text-anchor="middle">SEO</text>
</svg>
<svg width="604" height="76" x="200" y="250">
<g>
<rect fill="none" id="canvas_background" height="80" width="604" y="-1" x="-1"/>
<g display="none" overflow="visible" y="0" x="0" height="100%" width="100%" id="canvasGrid">
<rect fill="url(#gridpattern)" stroke-width="0" y="0" x="0" height="100%" width="100%"/>
</g>
</g>
<g>
<rect fill-opacity="0" stroke-width="2" rx="40" id="svg_2" height="72" width="600" y="1" x="0" fill="#000000"/>
<rect stroke="#000" rx="8" id="svg_3" height="14" width="48" y="30" x="35" stroke-opacity="null" stroke-width="0" fill="#ff4e42"/>
<rect stroke="#000" rx="6" id="svg_4" height="14" width="48" y="30" x="220" stroke-opacity="null" stroke-width="0" fill="#ffa400"/>
<rect stroke="#000" rx="6" id="svg_5" height="14" width="48" y="30" x="410" stroke-opacity="null" stroke-width="0" fill="#0cce6b"/>
<text class="metric guage-title" xml:space="preserve" text-anchor="start" font-size="26" id="svg_6" y="45" x="100" stroke-opacity="null" stroke-width="0" stroke="#000">0-49</text>
<text class="metric guage-title" xml:space="preserve" text-anchor="start" font-size="26" id="svg_7" y="45" x="280" stroke-opacity="null" stroke-width="0" stroke="#000">50-89</text>
<text class="metric guage-title" xml:space="preserve" text-anchor="start" font-size="26" id="svg_8" y="45" x="470" stroke-opacity="null" stroke-width="0" stroke="#000">90-100</text>
</g>
</svg>
</svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

459
CHANGELOG.md Normal file
View File

@ -0,0 +1,459 @@
# Changelog
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## v5.0.0 (2025-03-08)
### Feat
- add pagefind for static search (#458)
- update back button logic
### Fix
- ignore in eslint
- update blog table padding
- remove unused back url in the card url
- show light/dark button according to site setting
- add author url in Google JSON-LD conditionally
### Refactor
- remove react dependency for UI interactions (#457)
- separate config and constants
- update import alias in files
- update blog directory to `src/data/blog`
- upgrade to Tailwind CSS v4
- update import alias to `@/*`
- upgrade Astro to v5 and related packages
## v4.8.0 (2025-02-08)
### Feat
- add pencil icon before suggestion changes text (#405)
### Fix
- use tag name for display in tags page (#438)
- exclude `/archives` from sitemap if it is disabled (#425)
- add inline-block class to post title for improved view transition animation (#420)
- sort archive posts by pubDatetime (#415)
- focus search input on mount (#414)
- replace twitter with x (#407)
## v4.7.0 (2024-10-15)
### Feat
- add archives page with configurable menu (#386)
## v4.6.0 (2024-10-13)
### Feat
- add edit post feature in blog posts (#384)
### Refactor
- remove duplicate [page].astro (#389)
## v4.5.1 (2024-10-02)
### Fix
- **docs**: update giscus blog post (#392)
- add missing posts sorting (#383)
## v4.5.0 (2024-09-16)
### Feat
- add prev/next links at the bottom of blog post (#372)
### Fix
- **og**: add the missing SITE.website to loadGoogleFonts (#360)
- **blog**: correct file reference in reading time guide (#359)
### Refactor
- replace pagination logic with Astro built-in pagination (#376)
### Perf
- preload font and load theme script asynchronously (#380)
## v4.4.0 (2024-08-19)
### Content Layer API
- upgrade Astro and use Content Layer API (#355)
### Others
- upgrade ESLint to v9 and update configurations (#356)
- replace github-slugger with lodash.kebabcase (#357)
## v4.3.2 (2024-08-17)
### Fix
- **a11y**: remove aria-labels from non-interactive elements (#346)
### Refactor
- update tailwind classes to v3 syntax (#345)
- remove commented codes
### Others
- docs: update estimated reading time blog post (#354)
- docs: add instructions for Google Site Verification in AstroPaper (#353)
- docs: update pre-commit hook blog post (#344)
- ci: add CI workflow (#340)
## v4.3.1 (2024-07-27)
### Fix
- resolve non-latin char issue in generated OG images (#318)
## v4.3.0 (2024-07-27)
### Feat
- support light/dark theme in code blocks (#327)
- add number of posts config for home page (#281)
- make heading links keyboard focusable (#275)
- add JSON-LD structured data (#260)
- add scroll indicator in blog posts (#249)
### Fix
- adding data-theme to tailwind config (#319)
- avoid `undefined` when passing class-name as prop (#270)
- add $CURRENT_TIMEZONE_OFFSET in custom code snippets (#264)
- display `Updated` in posts only when modDatetime > pubDatetime (#258)
- add SITE.title in PostDetails title tag for consistent look (#247)
- add trailing slash to links to avoid extra redirects (#246)
- update incorrect typo in predefined-color-schemes.md (#245)
### Refactor
- remove trailing commas in tsconfig.json (#325)
- remove redundant role in article element (#323)
- avoid using unnecessary class-name in the pagination component (#274)
- update post detail script codes
- update code formatting with prettier
## [4.2.0](https://github.com/satnaing/astro-paper/compare/v4.1.0...v4.2.0) (2024-01-22)
### Features
* add heading links to PostDetails page ([#232](https://github.com/satnaing/astro-paper/issues/232)) ([742baff](https://github.com/satnaing/astro-paper/commit/742baff2c9bd47e0762f5d65f5b47a4d28014175))
* hide posts in Prod with future pubDatetime ([#234](https://github.com/satnaing/astro-paper/issues/234)) ([3efa05c](https://github.com/satnaing/astro-paper/commit/3efa05cc101688c32fc531af0122023d3ce82f08))
### Bug Fixes
* remove extra padding if lightAndDarkMode is false ([#230](https://github.com/satnaing/astro-paper/issues/230)) ([742314e](https://github.com/satnaing/astro-paper/commit/742314e0ac350a70ce1cc256e858c8de9c9153f6))
* reduce margin-bottom on markdown images ([#235](https://github.com/satnaing/astro-paper/issues/235)) ([1331795](https://github.com/satnaing/astro-paper/commit/1331795a4965aab5c47581c223f32f3ea2cd71ab))
* resolve broken line break in inline code ([#237](https://github.com/satnaing/astro-paper/issues/237)) ([ece0682](https://github.com/satnaing/astro-paper/commit/ece0682adce387f2a169185680cdf372a457e938))
* remove recent posts section if there's no post ([#238](https://github.com/satnaing/astro-paper/issues/238)) ([629dbfd](https://github.com/satnaing/astro-paper/commit/629dbfda5b99a71e629dbbf1845c3ceba5ac97e0)), closes [#204](https://github.com/satnaing/astro-paper/issues/204)
* update back button to redirect to home when no route history ([#241](https://github.com/satnaing/astro-paper/issues/241)) ([8f75f0a](https://github.com/satnaing/astro-paper/commit/8f75f0a5e75778a60e8030bb45b19289c0af502e))
### Others
* upgrade astro and other dependencies ([e903b69](https://github.com/satnaing/astro-paper/commit/e903b699cd947301256de1e62ae0ad2d1dcd3c2b))
* update code formatting with prettier ([424c422](https://github.com/satnaing/astro-paper/commit/424c422392d836516bfbb6004a234a1a57930be1))
* add astro extension in lint-staged code formatting ([d41bb69](https://github.com/satnaing/astro-paper/commit/d41bb69cd8f441caa773a07d911adb3ade54b493))
* update outdated prettier script ([1281b93](https://github.com/satnaing/astro-paper/commit/1281b9340a6bebd67628a8d4c56f318701ffde47))
## [4.1.0](https://github.com/satnaing/astro-paper/compare/v4.0.0...v4.1.0) (2024-01-10)
### Features
* update Astro and other dependencies ([f70a0b7](https://github.com/satnaing/astro-paper/commit/f70a0b78ed44350f6d1b00153ea0cc5b7d285043)) ([034dd39](https://github.com/satnaing/astro-paper/commit/034dd394abd4df5cb95fcfe975749cc535a6c05c))
* add share links in blog post ([#215](https://github.com/satnaing/astro-paper/issues/215))
* add copy buttons for code blocks ([#217](https://github.com/satnaing/astro-paper/issues/217))
### Bug Fixes
* resolve accessibility issues ([#226](https://github.com/satnaing/astro-paper/issues/226))
* solve modDatetime type errors ([#214](https://github.com/satnaing/astro-paper/issues/214))
* remove SocialObjects type and update SocialObjects type ([#225](https://github.com/satnaing/astro-paper/issues/225))
### Others
* adds blog post for how to add a social icon ([#221](https://github.com/satnaing/astro-paper/issues/221))
* updates the hook post with a smarter updateHook ([#222](https://github.com/satnaing/astro-paper/issues/222))
* update breadcrumbs delimiter to "»" ([#213](https://github.com/satnaing/astro-paper/issues/213))
## [4.0.0](https://github.com/satnaing/astro-paper/compare/v3.0.0...v4.0.0) (2024-01-04)
### ⚠ BREAKING CHANGES
* Astro v4 upgrade
### Features
* add code-snippets for content creation ([#206](https://github.com/satnaing/astro-paper/issues/206)) ([bb2f290](https://github.com/satnaing/astro-paper/commit/bb2f29008a96a0333e4c3adda202d20909728dfe))
* add docker-compose file ([#174](https://github.com/satnaing/astro-paper/issues/174)) ([fb3fa98](https://github.com/satnaing/astro-paper/commit/fb3fa98936d76331869641014d5b4568a84d8d42)), closes [#172](https://github.com/satnaing/astro-paper/issues/172)
* add image validation to schema ([e9d4303](https://github.com/satnaing/astro-paper/commit/e9d4303219bf312bc3955bb8af9aedb0eadb17cc))
* add modified datetime in blog posts ([80e67a1](https://github.com/satnaing/astro-paper/commit/80e67a1dcad19394d7b466472f3c674470db8e0c)), closes [#134](https://github.com/satnaing/astro-paper/issues/134)
* add pagination in tag posts ([#201](https://github.com/satnaing/astro-paper/issues/201)) ([581826a](https://github.com/satnaing/astro-paper/commit/581826a5affd03d12416a5ac7d28ed17d53eac8d)), closes [#152](https://github.com/satnaing/astro-paper/issues/152)
* add transition effect if light/dark changes ([a060cb5](https://github.com/satnaing/astro-paper/commit/a060cb5c87f733c455ea247d72f88095f1ca769c))
* add view transitions for card on search page ([#118](https://github.com/satnaing/astro-paper/issues/118)) ([6c7d04f](https://github.com/satnaing/astro-paper/commit/6c7d04fa12d006379157cf876c2826f606f124e9))
* add ViewTransitions from Astro ([cbdaa59](https://github.com/satnaing/astro-paper/commit/cbdaa59baea1c5c5497227dd2fb276e8cf88b936)), closes [#96](https://github.com/satnaing/astro-paper/issues/96)
* default post author to site author ([20c8970](https://github.com/satnaing/astro-paper/commit/20c89709ada7e7c3f49460d6690d02999ba86d17))
* dynamically generate robots.txt ([6352353](https://github.com/satnaing/astro-paper/commit/63523534703c1f95ac070452001070e2c3f74d5d))
* generate og image using templates ([3032c18](https://github.com/satnaing/astro-paper/commit/3032c18321dfd4f001bc86c094881219bd2e22b7))
* implement back-to-top button in blog post page ([c526157](https://github.com/satnaing/astro-paper/commit/c526157118b69ff68e3a653eee68428a791a7d9f)), closes [#156](https://github.com/satnaing/astro-paper/issues/156)
* og image routes ([300d014](https://github.com/satnaing/astro-paper/commit/300d014fd7a83f52020bdc21976de8487eb41f63))
* replace slugified title with unslugified tag name ([#198](https://github.com/satnaing/astro-paper/issues/198)) ([b05b8fb](https://github.com/satnaing/astro-paper/commit/b05b8fb842b43f4f6462b425cb46d835579cbcfb)), closes [#179](https://github.com/satnaing/astro-paper/issues/179)
* support custom canonical URLs ([#83](https://github.com/satnaing/astro-paper/issues/83)) ([4687bd5](https://github.com/satnaing/astro-paper/commit/4687bd516b16970fc4d163c1202b28f29818a582))
* update theme-color tag on theme switch ([f253776](https://github.com/satnaing/astro-paper/commit/f25377674ebc10f496ef6e5729b931d61ec67832))
### Bug Fixes
* [#133](https://github.com/satnaing/astro-paper/issues/133) update LOCALE config to cover overall locales ([cd02b04](https://github.com/satnaing/astro-paper/commit/cd02b047d2b5e3b4a2940c0ff30568cdebcec0b8))
* [#72](https://github.com/satnaing/astro-paper/issues/72) replace SITE.website with a URL in astro.config site value ([26ecd17](https://github.com/satnaing/astro-paper/commit/26ecd173ddec1075abb6ede9bbb62572b9f74b33))
* anchor oveflow on small screen size ([d025c91](https://github.com/satnaing/astro-paper/commit/d025c914d91a9b7969c8db4bd6a700723ef86a39))
* **css:** text wrap in code blocks ([0c92492](https://github.com/satnaing/astro-paper/commit/0c92492959bed20f144d5d949116891d61c8e098))
* decode unicode tag chars in breadcrumb ([#175](https://github.com/satnaing/astro-paper/issues/175)) ([058c790](https://github.com/satnaing/astro-paper/commit/058c790d26cbeab286679a8a8e3bad6c14042d6d))
* get og image url correctly ([7f3edbd](https://github.com/satnaing/astro-paper/commit/7f3edbdecdce597d15e562e7d497d69af505d550))
* **layout:** use 100svh for min-height on body instead of 100vh ([79d569d](https://github.com/satnaing/astro-paper/commit/79d569d053036f2113519f41b0d257523d035b76)), closes [#127](https://github.com/satnaing/astro-paper/issues/127)
* og image src ([6dffcf3](https://github.com/satnaing/astro-paper/commit/6dffcf3cb36a0dab6549ee249fe426b4ee931b06))
* prevent white flash in dark mode when navigate ([9eeb8fc](https://github.com/satnaing/astro-paper/commit/9eeb8fc76ecfd45b79ab716305f1916491649c95))
* remove empty string as ogImage ([b03b722](https://github.com/satnaing/astro-paper/commit/b03b7223694b4c215c6fce0a45ed4f03178081f4))
* resolve single-line code block wrapping issue ([#121](https://github.com/satnaing/astro-paper/issues/121)) ([0af3251](https://github.com/satnaing/astro-paper/commit/0af32518b343430dd8510470efd3806509337de7))
* solve invisible text code block issue in light-mode ([#163](https://github.com/satnaing/astro-paper/issues/163)) ([64b3a28](https://github.com/satnaing/astro-paper/commit/64b3a286e6e3ff1dff7cf4ca0fc8fafc222cabcd))
* sort posts in [tag] page ([#101](https://github.com/satnaing/astro-paper/issues/101)) ([b571816](https://github.com/satnaing/astro-paper/commit/b571816dcddc72a07147389090502c09025b28a6))
* update auto-gen OG images to allow special char usage in title ([1933a6b](https://github.com/satnaing/astro-paper/commit/1933a6beae7b4e2558b808d1f8a5c124f1244138)), closes [#103](https://github.com/satnaing/astro-paper/issues/103) [#88](https://github.com/satnaing/astro-paper/issues/88)
* update rss pubDate to prioritize modDatetime if exists ([e1514b4](https://github.com/satnaing/astro-paper/commit/e1514b41024bc10bcafcc4af548a6ebe0e093468))
* update tailwind base styles config ([#116](https://github.com/satnaing/astro-paper/issues/116)) ([4a03558](https://github.com/satnaing/astro-paper/commit/4a0355865081d07d05d9d758f520e411952a1063))
* update title of the blog nowrap ([87b3e5b](https://github.com/satnaing/astro-paper/commit/87b3e5b8cd7d424b3e43e6d5abed6d21195aa759))
* build!(deps): upgrade Astro and related packages to v4 ([a1d3ddd](https://github.com/satnaing/astro-paper/commit/a1d3ddd18591843a35b3c05be762e1f8af1b8fb0)), closes [#187](https://github.com/satnaing/astro-paper/issues/187)
## [3.0.0](https://github.com/satnaing/astro-paper/compare/v2.3.0...v3.0.0) (2023-09-25)
### ⚠ BREAKING CHANGES
* Astro v3
> Check the AstroPaper v3 in [this blog post](https://astro-paper.pages.dev/posts/astro-paper-v3/)
### Features
* upgrade to astro v3 ([8fda50f](https://github.com/satnaing/astro-paper/commit/8fda50f5ddb7130b7954ad217eed1848094ee33c)), closes [#111](https://github.com/satnaing/astro-paper/issues/111)
* add view transitions for card on search page ([#118](https://github.com/satnaing/astro-paper/issues/118)) ([b873ed5](https://github.com/satnaing/astro-paper/commit/b873ed5a07e746404960690669e8960c2a4c628d))
* add ViewTransitions from Astro ([9703e54](https://github.com/satnaing/astro-paper/commit/9703e54ca4264b0437e06c45bbcc53a7a7d1e106)), closes [#96](https://github.com/satnaing/astro-paper/issues/96)
([b873ed5](https://github.com/satnaing/astro-paper/commit/b873ed5a07e746404960690669e8960c2a4c628d)), closes [#96](https://github.com/satnaing/astro-paper/issues/96)
* generate OG image using templates ([0f82206](https://github.com/satnaing/astro-paper/commit/0f822060cec82b218b568e9ef311fe6adc8b7a1e))
* support custom canonical URLs ([#83](https://github.com/satnaing/astro-paper/issues/83)) ([4687bd5](https://github.com/satnaing/astro-paper/commit/4687bd516b16970fc4d163c1202b28f29818a582))
* update theme-color tag on theme switch ([b5f5418](https://github.com/satnaing/astro-paper/commit/b5f54180c8645113ae4e177f3ebb97e1947dc9e2))
* use new og images in layout ([ec3c691](https://github.com/satnaing/astro-paper/commit/ec3c69114f7344b27797853e2e5a573feb5c63fc))
### Bug Fixes
* replace SITE.website with a URL in astro.config site value ([26ecd17](https://github.com/satnaing/astro-paper/commit/26ecd173ddec1075abb6ede9bbb62572b9f74b33)), fixes [#72](https://github.com/satnaing/astro-paper/issues/72)
* **css:** make code scrollable in code blocks ([0c92492](https://github.com/satnaing/astro-paper/commit/0c92492959bed20f144d5d949116891d61c8e098))
* remove empty string as ogImage ([5259994](https://github.com/satnaing/astro-paper/commit/5259994525b0b67a584b4268a3fbb74258871a3a))
* resolve single-line code block wrapping issue ([#121](https://github.com/satnaing/astro-paper/issues/121)) ([8f08018](https://github.com/satnaing/astro-paper/commit/8f0801836a589133932dc5a450060fd2f16daf74))
* sort posts in [tag] page ([#101](https://github.com/satnaing/astro-paper/issues/101)) ([b571816](https://github.com/satnaing/astro-paper/commit/b571816dcddc72a07147389090502c09025b28a6))
* update auto-gen OG images to allow special char usage in title ([f26bf85](https://github.com/satnaing/astro-paper/commit/f26bf8581288523a0d6021a141cdada685fbce46)), closes [#103](https://github.com/satnaing/astro-paper/issues/103) [#88](https://github.com/satnaing/astro-paper/issues/88)
* update tailwind base styles config ([#116](https://github.com/satnaing/astro-paper/issues/116)) ([98a2bb6](https://github.com/satnaing/astro-paper/commit/98a2bb682af2773d6af7782a6592e9b9fab79b3b))
* update title of the blog nowrap ([2df71b9](https://github.com/satnaing/astro-paper/commit/2df71b9b4587c7a2438f483e8365ef5b8a502ce7))
## [2.3.0](https://github.com/satnaing/astro-paper/compare/v2.2.0...v2.3.0) (2023-05-15)
### Features
* add locale configuration for Datetime component ([#59](https://github.com/satnaing/astro-paper/issues/59)) ([0e9f709](https://github.com/satnaing/astro-paper/commit/0e9f709c5dbd9a75aaf33e7994e88216fd56d8be))
### Bug Fixes
* add missing sitemap in head ([#69](https://github.com/satnaing/astro-paper/issues/69)) ([f6ac810](https://github.com/satnaing/astro-paper/commit/f6ac8104b2ba20de3b71eb5dde395e5adce9dfe7))
* build error astro@2.1.4 && update astro@2.1.5 ([#49](https://github.com/satnaing/astro-paper/issues/49)) ([dd4fd98](https://github.com/satnaing/astro-paper/commit/dd4fd989722cbcb3e98045e808a32292cf555900))
* **ignore:** added yarn directories to ignorefiles ([f3e9cd5](https://github.com/satnaing/astro-paper/commit/f3e9cd51479fd41f3c0e8863ac13c77d6daa2605))
* replace history entries when searching ([#62](https://github.com/satnaing/astro-paper/issues/62)) ([a57f343](https://github.com/satnaing/astro-paper/commit/a57f3439f801c1d41256a8a46bd319c17dff86f1))
* slugify tags in post detail page ([49d7f77](https://github.com/satnaing/astro-paper/commit/49d7f77a86987c00d211090301b662e21a27ce17))
* sort rss feed from latest to oldest ([#38](https://github.com/satnaing/astro-paper/issues/38)) ([9e62b63](https://github.com/satnaing/astro-paper/commit/9e62b637e8ddb65f5f274fd0154191212dda0590))
* tailwind jsdoc for intellisense ([99709dd](https://github.com/satnaing/astro-paper/commit/99709dd3aa2329220a497f7038b7ab069d389847))
* update lint-staged configuration ([e654c03](https://github.com/satnaing/astro-paper/commit/e654c0308c26ccffdd0a4abc50f0adb99c76d9ca)), closes [#52](https://github.com/satnaing/astro-paper/issues/52)
* update menu element with svg and refactor toggle logic ([0f76424](https://github.com/satnaing/astro-paper/commit/0f764242fea14565798085447d8524b4bf05f76a))
## [2.2.0](https://github.com/satnaing/astro-paper/compare/v2.1.0...v2.2.0) (2023-03-16)
### Features
* generate og images in png format ([#43](https://github.com/satnaing/astro-paper/issues/43)) ([27507d1](https://github.com/satnaing/astro-paper/commit/27507d1d78531901c20a17d9ce72728c6cbb521e)), closes [#40](https://github.com/satnaing/astro-paper/issues/40)
### Bug Fixes
* add plugin-search-dir in prettier write for pnpm ([e49ca61](https://github.com/satnaing/astro-paper/commit/e49ca61d6b7048a8e8b2f50b1d947fd91eaca3eb)), ([37b54af](https://github.com/satnaing/astro-paper/commit/37b54afd9471eb35588e09f1f33ae1634732b02c)), closes [#34](https://github.com/satnaing/astro-paper/issues/34)
* correct typo in blog posts ([cbce54b](https://github.com/satnaing/astro-paper/commit/cbce54bd1cf951c36a8603db8f7a8487481fc7f1)), closes [#35](https://github.com/satnaing/astro-paper/issues/35)
* slugifyAll typo ([bcae985](https://github.com/satnaing/astro-paper/commit/bcae9856712773887664bb3a3392e1ebfd78607b))
### Others
* update Astro to v2.1.3 and enable type checking in dev ([329bc22](https://github.com/satnaing/astro-paper/commit/329bc22e97892e5687a841d580215c8fb2d44aa1))
* add jampack for performance optimization ([#46](https://github.com/satnaing/astro-paper/pull/46)) ([b9254c1](https://github.com/satnaing/astro-paper/commit/b9254c15f1b382c2f3900b3371abce8975768dd9))
## [2.1.0](https://github.com/satnaing/astro-paper/compare/v2.0.0...v2.1.0) (2023-02-08)
### Features
* add ESLint and update linting errors ([#26](https://github.com/satnaing/astro-paper/issues/26)) ([a9631d0](https://github.com/satnaing/astro-paper/commit/a9631d0e1e65ac4339c6b4d806b3a17928fa2b62))
### Bug Fixes
* make schema(s) strict ([#23](https://github.com/satnaing/astro-paper/issues/23)) ([dc026b3](https://github.com/satnaing/astro-paper/commit/dc026b38defa760d77eddcddb1d4f12fdf8fff99))
* fix typo and remove unnecessary comments ([#24](https://github.com/satnaing/astro-paper/pull/24)) ([d9a2ffe](https://github.com/satnaing/astro-paper/commit/d9a2ffe9096e2419a740c5b98b57323fbf2f2cb0)) ([#25](https://github.com/satnaing/astro-paper/pull/25)) ([29e0776](https://github.com/satnaing/astro-paper/commit/29e07761f78fa24b307601bf2272a61e084a468b))
* update dependencies
## [2.0.0](https://github.com/satnaing/astro-paper/compare/v1.4.0...v2.0.0) (2023-01-31)
### ⚠ BREAKING CHANGES
Check the AstroPaper v2 in [this blog post](https://astro-paper.pages.dev/posts/astro-paper-2/)
* **deps:** Migration of Astro to version 2
### Features
* add Mastodon social link ([2ec3912](https://github.com/satnaing/astro-paper/commit/2ec39128c65fd0b1dafd6aebd48ac3068f40f9c5))
* add new predefined color scheme 'astro dark' ([bc263b6](https://github.com/satnaing/astro-paper/commit/bc263b6eac00fbc8ec62481f2ec0317ee11bc83a))
* define blog schema and add blog collection ([b420e68](https://github.com/satnaing/astro-paper/commit/b420e688ca3a197a7e4ea2591193fd09da817ec7))
### Bug Fixes
* add embedFont option for Satori ([9322123](https://github.com/satnaing/astro-paper/commit/93221239ddaebaa9ab183871cf978548ea8d0ea5))
* exclude draft posts in specific tag page ([c192cd8](https://github.com/satnaing/astro-paper/commit/c192cd8e5042d4481bcb0d0389866cf4a969aa8d))
* fix broken tags in PostDetails page ([a61fd45](https://github.com/satnaing/astro-paper/commit/a61fd455594932c66380a358b81b8bebb9d604cc))
* fix typo in title and slug ([945acf4](https://github.com/satnaing/astro-paper/commit/945acf4260e0ea79bde8b180835049eda07d3e6a))
* hide social links section if no link is active ([42eb018](https://github.com/satnaing/astro-paper/commit/42eb0188896a8475a7fbb894775e5500ca8b7d35)), closes [#16](https://github.com/satnaing/astro-paper/issues/16)
* make the last part of breadcrumb lowercase in specific tag page ([c556202](https://github.com/satnaing/astro-paper/commit/c556202c972f1f9fed9af0ba6abf199e7deccc5f))
* resolve initial onChange input value bug ([bf4f687](https://github.com/satnaing/astro-paper/commit/bf4f687d2d87cfeef96141c5324d02c37766845b))
* update card bg color ([8a99601](https://github.com/satnaing/astro-paper/commit/8a99601e93f90c0870a22aa4a8ea8b7ff1b76a98))
* use default-og for twitter card ([9434d85](https://github.com/satnaing/astro-paper/commit/9434d850e1f41f0802de5706c4c5712e5b5def9d))
### build
* **deps:** bump astro and its packages to v2 ([5f279b3](https://github.com/satnaing/astro-paper/commit/5f279b34f88bd94bed820d16c1e1d5e95859045f))
## [1.4.0](https://github.com/satnaing/astro-paper/compare/v1.3.0...v1.4.0) (2022-12-28)
### Features
* generate dynamic og image for blog posts ([#15](https://github.com/satnaing/astro-paper/issues/15)) ([ce3f1dc](https://github.com/satnaing/astro-paper/commit/ce3f1dc4a0df8f196dce37de1c976870e9c97279))
### Bug Fixes
* fix grammar mistake ([02faff9](https://github.com/satnaing/astro-paper/commit/02faff9fbd4444144eeb139ae62850ec5a980dd3))
## [1.3.0](https://github.com/satnaing/astro-paper/compare/v1.2.1...v1.3.0) (2022-12-07)
### Features
* update mobile nav to be accessible ([46ea4aa](https://github.com/satnaing/astro-paper/commit/46ea4aa49a49a3d21ca5ce1cee1b51f0108c13f0))
### [1.2.1](https://github.com/satnaing/astro-paper/compare/v1.2.0...v1.2.1) (2022-12-02)
### Bug Fixes
* disable access to draft posts via url ([1c2821e](https://github.com/satnaing/astro-paper/commit/1c2821e4df65bee7126aed17244bb6590b1163d8))
* display '0 results' instead of '0 result' in Search ([eceb289](https://github.com/satnaing/astro-paper/commit/eceb2895623cffefc65671fdfc343fa5e4c01cdb))
* displays featured section only if featured posts exist ([e0f93da](https://github.com/satnaing/astro-paper/commit/e0f93dab02024d65ddb69925a21e8d8598a036e9))
* fix calculating draft posts in totalPages ([19e34a0](https://github.com/satnaing/astro-paper/commit/19e34a0801019df8681d1d4e80f678989cf2457c))
* hide pagination when there's only 1 page ([6b35c7f](https://github.com/satnaing/astro-paper/commit/6b35c7fc2f63bb16aaefc140029b1eae1235cc44))
## [1.2.0](https://github.com/satnaing/astro-paper/compare/v1.1.3...v1.2.0) (2022-11-28)
### Features
* improve accessibility including voiceover ([5860254](https://github.com/satnaing/astro-paper/commit/5860254ea99996e466f2e521f033763961b6faa6))
* add linkTitle in social links ([c9f796f](https://github.com/satnaing/astro-paper/commit/c9f796f4e63f1cf6b32b7874ae5e3810598a230c))
### Updates
* move toggle theme codes from `layouts/Layout.astro` to `toggle-theme.js` ([5860254](https://github.com/satnaing/astro-paper/commit/5860254ea99996e466f2e521f033763961b6faa6))
* delete `utils/formatDatetime.ts` and replaced with `FormattedDatetime` inside `components/Datetime.tsx` ([0eeed8e](https://github.com/satnaing/astro-paper/commit/0eeed8e870781d9b4a447c51e3055ccb2f359d8a))
* 'toggling light and dark mode' code is remove from `src/components/Header.astro` and is rewritten in `public/toggle-theme.js` file. ([2ba459b](https://github.com/satnaing/astro-paper/commit/2ba459b4131a11a68a5fd818a278c474c1888cde)) ([0eeed8e](https://github.com/satnaing/astro-paper/commit/0eeed8e870781d9b4a447c51e3055ccb2f359d8a))
* update previous and next button disabled state ([408fc4c](https://github.com/satnaing/astro-paper/commit/408fc4c7aa5a246fe82a6e85d119b36ee1f1ffc3))
* **typo:** rename Linkedin to LinkedIn ([307b55f](https://github.com/satnaing/astro-paper/commit/307b55ff0f6cb86a4fa4152c635d6acb39d1512f))
* update patch and minor dependencies ([3b0ab75](https://github.com/satnaing/astro-paper/commit/3b0ab7555f506a8a0b825ca9691fdb221e481adb)) ([c3a6e4e](https://github.com/satnaing/astro-paper/commit/c3a6e4e81d1f79efc17d451486ff560dccb8ddf0))
### [1.1.3](https://github.com/satnaing/astro-paper/compare/v1.1.2...v1.1.3) (2022-11-11)
### Bug Fixes
* fix broken post links and hide draft posts in rss feed ([b83c906](https://github.com/satnaing/astro-paper/commit/b83c906262cb5e1f045ac50f2401527c0b64074c))
### [1.1.2](https://github.com/satnaing/astro-paper/compare/v1.1.1...v1.1.2) (2022-11-04)
### Bug Fixes
* fix heading style in posts/<page-num> layouts ([5eeea66](https://github.com/satnaing/astro-paper/commit/5eeea6639e79f93c3d0917bc827dfd37a23d041c))
* fix missing TailwindCSS dependency ([e7807ab](https://github.com/satnaing/astro-paper/commit/e7807ab94e12898ab85b955132c5d908956c8945)), closes [#6](https://github.com/satnaing/astro-paper/issues/6)
* show search result only if input is more than one char ([f7fb032](https://github.com/satnaing/astro-paper/commit/f7fb032e604bd704adc19400e000c9584a6fdb43))
### [1.1.1](https://github.com/satnaing/astro-paper/compare/v1.1.0...v1.1.1) (2022-10-30)
### Updates
* update github-slugger by @AkaraChen in https://github.com/satnaing/astro-paper/pull/5
* move '@types/react' to dev dependencies ([3697a59](https://github.com/satnaing/astro-paper/commit/3697a59f1ab8b58af7d41c2ef4aa8ba97b9ad1e2))
* update dependencies
## [1.1.0](https://github.com/satnaing/astro-paper/compare/v1.0.1...v1.1.0) (2022-10-18)
### Features
* improve search functionality ([33bab9c](https://github.com/satnaing/astro-paper/commit/33bab9c489d74e1b53109d5f1e8f3586cfcb9433))
* add CHANGELOG ([adb331e](https://github.com/satnaing/astro-paper/commit/adb331e219d122be696fb390ae41f0afaa5a76b9))
* add prettier and husky ([d6dd818](https://github.com/satnaing/astro-paper/commit/d6dd8185f28cfae967cf90c9020580ebce5c36fd) | [80aee6b](https://github.com/satnaing/astro-paper/commit/80aee6bedbc1e40650411b0695f5365902d3b9e2))
### Bug Fixes
* fix markdown lint warnings by updating headers ([ad14dc5](https://github.com/satnaing/astro-paper/commit/ad14dc580fbf886f5de95705ec7910c7c3b46bf0))
* fix markdown warnings by adding alt texts ([3260641](https://github.com/satnaing/astro-paper/commit/326064111cbb7d356659252dd7ddd42dbd2d7e56))
* extract Social component to avoid duplication ([7ef631f](https://github.com/satnaing/astro-paper/commit/7ef631fe35dc57db1c84e7c3c92969fa23ccd42b))
* update glob to have access to sub directories under content/ ([a256ded](https://github.com/satnaing/astro-paper/commit/a256dedb73aaf018cedf764f38843ad176b27058))
## [1.0.1](https://github.com/satnaing/astro-paper/compare/v1.0.0...v1.0.1) (2022-09-27) Initial Release
### Features
- Fully responsive & accessible
- Pagination & draft post
- Light & dark color schemes
- 19 social link icons
- Fuzzy search
- Sitemap & RSS feed
- 5 predefined themes

17
Dockerfile Normal file
View File

@ -0,0 +1,17 @@
# Base stage for building the static files
FROM node:lts AS base
WORKDIR /app
# Install pnpm
RUN corepack enable && corepack prepare pnpm@latest --activate
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm run build
# Runtime stage for serving the application
FROM nginx:mainline-alpine-slim AS runtime
COPY --from=base /app/dist /usr/share/nginx/html
EXPOSE 80

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Sat Naing
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

177
README.md Normal file
View File

@ -0,0 +1,177 @@
# AstroPaper 📄
![AstroPaper](public/astropaper-og.jpg)
[![Figma](https://img.shields.io/badge/Figma-F24E1E?style=for-the-badge&logo=figma&logoColor=white)](https://www.figma.com/community/file/1356898632249991861)
![Typescript](https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white)
![GitHub](https://img.shields.io/github/license/satnaing/astro-paper?color=%232F3741&style=for-the-badge)
[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-%23FE5196?logo=conventionalcommits&logoColor=white&style=for-the-badge)](https://conventionalcommits.org)
[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg?style=for-the-badge)](http://commitizen.github.io/cz-cli/)
AstroPaper is a minimal, responsive, accessible and SEO-friendly Astro blog theme. This theme is designed and crafted based on [my personal blog](https://satnaing.dev/blog).
Read [the blog posts](https://astro-paper.pages.dev/posts/) or check [the README Documentation Section](#-documentation) for more info.
## 🔥 Features
- [x] type-safe markdown
- [x] super fast performance
- [x] accessible (Keyboard/VoiceOver)
- [x] responsive (mobile ~ desktops)
- [x] SEO-friendly
- [x] light & dark mode
- [x] fuzzy search
- [x] draft posts & pagination
- [x] sitemap & rss feed
- [x] followed best practices
- [x] highly customizable
- [x] dynamic OG image generation for blog posts [#15](https://github.com/satnaing/astro-paper/pull/15) ([Blog Post](https://astro-paper.pages.dev/posts/dynamic-og-image-generation-in-astropaper-blog-posts/))
_Note: I've tested screen-reader accessibility of AstroPaper using **VoiceOver** on Mac and **TalkBack** on Android. I couldn't test all other screen-readers out there. However, accessibility enhancements in AstroPaper should be working fine on others as well._
## ✅ Lighthouse Score
<p align="center">
<a href="https://pagespeed.web.dev/report?url=https%3A%2F%2Fastro-paper.pages.dev%2F&form_factor=desktop">
<img width="710" alt="AstroPaper Lighthouse Score" src="AstroPaper-lighthouse-score.svg">
<a>
</p>
## 🚀 Project Structure
Inside of AstroPaper, you'll see the following folders and files:
```bash
/
├── public/
│ ├── assets/
| ├── pagefind/ # auto-generated when build
│ └── favicon.svg
│ └── astropaper-og.jpg
│ └── favicon.svg
│ └── toggle-theme.js
├── src/
│ ├── assets/
│ │ └── icons/
│ │ └── images/
│ ├── components/
│ ├── data/
│ │ └── blog/
│ │ └── some-blog-posts.md
│ ├── layouts/
│ └── pages/
│ └── styles/
│ └── utils/
│ └── config.ts
│ └── constants.ts
│ └── content.config.ts
└── astro.config.ts
```
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
Any static assets, like images, can be placed in the `public/` directory.
All blog posts are stored in `src/data/blog` directory.
## 📖 Documentation
Documentation can be read in two formats\_ _markdown_ & _blog post_.
- Configuration - [markdown](src/data/blog/how-to-configure-astropaper-theme.md) | [blog post](https://astro-paper.pages.dev/posts/how-to-configure-astropaper-theme/)
- Add Posts - [markdown](src/data/blog/adding-new-post.md) | [blog post](https://astro-paper.pages.dev/posts/adding-new-posts-in-astropaper-theme/)
- Customize Color Schemes - [markdown](src/data/blog/customizing-astropaper-theme-color-schemes.md) | [blog post](https://astro-paper.pages.dev/posts/customizing-astropaper-theme-color-schemes/)
- Predefined Color Schemes - [markdown](src/data/blog/predefined-color-schemes.md) | [blog post](https://astro-paper.pages.dev/posts/predefined-color-schemes/)
## 💻 Tech Stack
**Main Framework** - [Astro](https://astro.build/)
**Type Checking** - [TypeScript](https://www.typescriptlang.org/)
**Styling** - [TailwindCSS](https://tailwindcss.com/)
**UI/UX** - [Figma Design File](https://www.figma.com/community/file/1356898632249991861)
**Static Search** - [FuseJS](https://pagefind.app/)
**Icons** - [Tablers](https://tabler-icons.io/)
**Code Formatting** - [Prettier](https://prettier.io/)
**Deployment** - [Cloudflare Pages](https://pages.cloudflare.com/)
**Illustration in About Page** - [https://freesvgillustration.com](https://freesvgillustration.com/)
**Linting** - [ESLint](https://eslint.org)
## 👨🏻‍💻 Running Locally
You can start using this project locally by running the following command in your desired directory:
```bash
# pnpm
pnpm create astro@latest --template satnaing/astro-paper
# npm
npm create astro@latest -- --template satnaing/astro-paper
# yarn
yarn create astro --template satnaing/astro-paper
```
Then start the project by running the following commands:
```bash
# install dependencies if you haven't done so in the previous step.
pnpm install
# start running the project
pnpm run dev
```
As an alternative approach, if you have Docker installed, you can use Docker to run this project locally. Here's how:
```bash
# Build the Docker image
docker build -t astropaper .
# Run the Docker container
docker run -p 4321:80 astropaper
```
## Google Site Verification (optional)
You can easily add your [Google Site Verification HTML tag](https://support.google.com/webmasters/answer/9008080#meta_tag_verification&zippy=%2Chtml-tag) in AstroPaper using an environment variable. This step is optional. If you don't add the following environment variable, the google-site-verification tag won't appear in the HTML `<head>` section.
```bash
# in your environment variable file (.env)
PUBLIC_GOOGLE_SITE_VERIFICATION=your-google-site-verification-value
```
> See [this discussion](https://github.com/satnaing/astro-paper/discussions/334#discussioncomment-10139247) for adding AstroPaper to the Google Search Console.
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
> **_Note!_** For `Docker` commands we must have it [installed](https://docs.docker.com/engine/install/) in your machine.
| Command | Action |
| :----------------------------------- | :------------------------------------------------------------------------------------------------------------------------------- |
| `pnpm install` | Installs dependencies |
| `pnpm run dev` | Starts local dev server at `localhost:4321` |
| `pnpm run build` | Build your production site to `./dist/` |
| `pnpm run preview` | Preview your build locally, before deploying |
| `pnpm run format:check` | Check code format with Prettier |
| `pnpm run format` | Format codes with Prettier |
| `pnpm run sync` | Generates TypeScript types for all Astro modules. [Learn more](https://docs.astro.build/en/reference/cli-reference/#astro-sync). |
| `pnpm run lint` | Lint with ESLint |
| `docker compose up -d` | Run AstroPaper on docker, You can access with the same hostname and port informed on `dev` command. |
| `docker compose run app npm install` | You can run any command above into the docker container. |
| `docker build -t astropaper .` | Build Docker image for AstroPaper. |
| `docker run -p 4321:80 astropaper` | Run AstroPaper on Docker. The website will be accessible at `http://localhost:4321`. |
> **_Warning!_** Windows PowerShell users may need to install the [concurrently package](https://www.npmjs.com/package/concurrently) if they want to [run diagnostics](https://docs.astro.build/en/reference/cli-reference/#astro-check) during development (`astro check --watch & astro dev`). For more info, see [this issue](https://github.com/satnaing/astro-paper/issues/113).
## ✨ Feedback & Suggestions
If you have any suggestions/feedback, you can contact me via [my email](mailto:contact@satnaing.dev). Alternatively, feel free to open an issue if you find bugs or want to request new features.
## 📜 License
Licensed under the MIT License, Copyright © 2025
---
Made with 🤍 by [Sat Naing](https://satnaing.dev) 👨🏻‍💻 and [contributors](https://github.com/satnaing/astro-paper/graphs/contributors).

39
astro.config.ts Normal file
View File

@ -0,0 +1,39 @@
import { defineConfig } from "astro/config";
import tailwindcss from "@tailwindcss/vite";
import sitemap from "@astrojs/sitemap";
import remarkToc from "remark-toc";
import remarkCollapse from "remark-collapse";
import { SITE } from "./src/config";
// https://astro.build/config
export default defineConfig({
site: SITE.website,
integrations: [
sitemap({
filter: page => SITE.showArchives || !page.endsWith("/archives"),
}),
],
markdown: {
remarkPlugins: [remarkToc, [remarkCollapse, { test: "Table of contents" }]],
shikiConfig: {
// For more themes, visit https://shiki.style/themes
themes: { light: "min-light", dark: "night-owl" },
wrap: true,
},
},
vite: {
plugins: [tailwindcss()],
optimizeDeps: {
exclude: ["@resvg/resvg-js"],
},
},
image: {
// Used for all Markdown images; not configurable per-image
// Used for all `<Image />` and `<Picture />` components unless overridden with a prop
experimentalLayout: "responsive",
},
experimental: {
svg: true,
responsiveImages: true,
},
});

7
cz.yaml Normal file
View File

@ -0,0 +1,7 @@
---
commitizen:
name: cz_conventional_commits
tag_format: v$version
update_changelog_on_bump: true
version_provider: npm
version_scheme: semver

11
docker-compose.yml Normal file
View File

@ -0,0 +1,11 @@
version: '3'
services:
app:
image: node:20
ports:
- 4321:4321
working_dir: /app
command: npm run dev -- --host 0.0.0.0
volumes:
- ./:/app

18
eslint.config.js Normal file
View File

@ -0,0 +1,18 @@
import eslintPluginAstro from "eslint-plugin-astro";
import globals from "globals";
import tseslint from "typescript-eslint";
export default [
...tseslint.configs.recommended,
...eslintPluginAstro.configs.recommended,
{
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
},
},
{ rules: { "no-console": "error" } },
{ ignores: ["dist/**", ".astro", "public/pagefind/**"] },
];

52
package.json Normal file
View File

@ -0,0 +1,52 @@
{
"name": "astro-paper",
"type": "module",
"version": "5.0.0",
"scripts": {
"dev": "astro dev",
"build": "astro check && astro build && pagefind --site dist && cp -r dist/pagefind public/",
"preview": "astro preview",
"sync": "astro sync",
"astro": "astro",
"format:check": "prettier --check .",
"format": "prettier --write .",
"lint": "eslint ."
},
"dependencies": {
"@astrojs/rss": "^4.0.11",
"@astrojs/sitemap": "^3.2.1",
"@resvg/resvg-js": "^2.6.2",
"@tailwindcss/vite": "^4.0.12",
"astro": "^5.4.2",
"lodash.kebabcase": "^4.1.1",
"remark-collapse": "^0.1.2",
"remark-toc": "^9.0.0",
"satori": "^0.12.1",
"sharp": "^0.33.5",
"tailwindcss": "^4.0.12"
},
"devDependencies": {
"@astrojs/check": "^0.9.4",
"@pagefind/default-ui": "^1.3.0",
"@tailwindcss/typography": "^0.5.16",
"@types/lodash.kebabcase": "^4.1.9",
"@typescript-eslint/parser": "^8.26.0",
"eslint": "^9.22.0",
"eslint-plugin-astro": "^1.3.1",
"globals": "^16.0.0",
"pagefind": "^1.3.0",
"prettier": "^3.5.3",
"prettier-plugin-astro": "^0.14.1",
"prettier-plugin-tailwindcss": "^0.6.11",
"typescript": "^5.8.2",
"typescript-eslint": "^8.26.0"
},
"pnpm": {
"onlyBuiltDependencies": [
"esbuild"
],
"ignoredBuiltDependencies": [
"sharp"
]
}
}

5575
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
public/astropaper-og.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

BIN
public/avatar.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

361
public/dev.svg Normal file
View File

@ -0,0 +1,361 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" width="865.76" height="682.89" viewBox="0 0 865.76 682.89">
<defs>
<style xmlns="http://www.w3.org/1999/xhtml">*, body, html { -webkit-font-smoothing: antialiased; }
img, svg { max-width: 100%; }
</style>
</defs>
<path d="M391.82,532.2c-15.44,2.82-87.85,18.09-73.28,55a33.24,33.24,0,0,0,9.74,13.13c18.18,15.25,83.33,52.58,272.06,32.22,10.69-1.15,21.42-1.86,32.17-2.06,49.73-.92,206-9.34,202-78.54,0,0-2.07-38.74-95.7-26.87l-71.21-4.43s-160.55-12.38-268.7,10.11C396.57,531.29,394.2,531.77,391.82,532.2Z" fill="#787878" data-primary="true"/>
<path d="M391.82,532.2c-15.44,2.82-87.85,18.09-73.28,55a33.24,33.24,0,0,0,9.74,13.13c18.18,15.25,83.33,52.58,272.06,32.22,10.69-1.15,21.42-1.86,32.17-2.06,49.73-.92,206-9.34,202-78.54,0,0-2.07-38.74-95.7-26.87l-71.21-4.43s-160.55-12.38-268.7,10.11C396.57,531.29,394.2,531.77,391.82,532.2Z" fill="#fff" opacity="0.7"/>
<path d="M503.08,522.3C179.26,552.79,133.91,359.63,133.91,359.63c-24.79-67.13-3.45-111,27.66-152.68a303.36,303.36,0,0,1,117.77-94.5c74.9-34.06,126.36-41,126.36-41S622.58,15.68,735.64,183.7c0,0,108,135.55,37.54,221.14,0,0-35.14,47.34-127.63,82.89l-69.3,20.46A387.7,387.7,0,0,1,503.08,522.3Z" fill="#787878" data-primary="true"/>
<path d="M503.08,522.3C179.26,552.79,133.91,359.63,133.91,359.63c-24.79-67.13-3.45-111,27.66-152.68a303.36,303.36,0,0,1,117.77-94.5c74.9-34.06,126.36-41,126.36-41S622.58,15.68,735.64,183.7c0,0,108,135.55,37.54,221.14,0,0-35.14,47.34-127.63,82.89l-69.3,20.46A387.7,387.7,0,0,1,503.08,522.3Z" fill="#fff" opacity="0.7"/>
<rect x="104.67" y="206.46" width="463.2" height="348.88" fill="#fff"/>
<rect x="108.43" y="206.46" width="459.44" height="35.42" fill="#e6e6e6"/>
<rect x="128.82" y="259.06" width="104.13" height="104.13" fill="#787878" opacity="0.29" data-primary="true"/>
<rect x="713.86" y="369.62" width="5.37" height="37.57" fill="#999"/>
<polygon points="664.89 442.18 664.89 554.44 672.53 554.44 676.93 436.58 664.89 442.18" fill="#ccc"/>
<polygon points="711.71 420.08 711.71 537.08 719.36 537.08 723.52 414.71 711.71 420.08" fill="#ccc"/>
<polygon points="668.23 434.1 733.18 405.05 703.86 399.96 670.01 385.44 668.23 434.1" fill="#ccc"/>
<path d="M656.14,446.25l77-35.83v-5.37L668.23,434.1S660.68,442.36,656.14,446.25Z" fill="#b3b3b3"/>
<path d="M693.46,271.94H734a4.55,4.55,0,0,1,4.55,4.55v67.37a0,0,0,0,1,0,0H693.46a0,0,0,0,1,0,0V271.94A0,0,0,0,1,693.46,271.94Z" fill="#999"/>
<rect x="241.54" y="44.36" width="325.8" height="139.55" fill="#787878" data-primary="true"/>
<rect x="263.01" y="83.01" width="100.91" height="65.48" fill="#fff" opacity="0.3"/>
<g opacity="0.3">
<path d="M297.36,131.59a1.07,1.07,0,0,1-.76-.32l-14.79-14.76a1.08,1.08,0,0,1,0-1.5l14.79-15.56a1.07,1.07,0,0,1,1.56,1.47l-14.07,14.81,14.05,14a1.07,1.07,0,0,1,0,1.52A1.09,1.09,0,0,1,297.36,131.59Z" fill="#fff"/>
</g>
<g opacity="0.3">
<path d="M328.73,132.66a1.06,1.06,0,0,1-.76-.31,1.07,1.07,0,0,1,0-1.52l14-14L328,102a1.08,1.08,0,1,1,1.56-1.48l14.78,15.56a1.06,1.06,0,0,1,0,1.5l-14.78,14.77A1.07,1.07,0,0,1,328.73,132.66Z" fill="#fff"/>
</g>
<g opacity="0.3">
<path d="M305.56,131.59a1.08,1.08,0,0,1-1-1.56l14.34-28.18a1.08,1.08,0,1,1,1.92,1L306.51,131A1.07,1.07,0,0,1,305.56,131.59Z" fill="#fff"/>
</g>
<path d="M524.39,119.51H454.62a1.08,1.08,0,0,1,0-2.15h69.77a1.08,1.08,0,1,1,0,2.15Z" fill="#fff"/>
<path d="M540.5,132.39H454.62a1.08,1.08,0,0,1,0-2.15H540.5a1.08,1.08,0,0,1,0,2.15Z" fill="#fff"/>
<rect x="460.52" y="153.86" width="65.48" height="16.1" rx="7.5" fill="#fff" opacity="0.3"/>
<path d="M567.33,44.36V183.91H241.54s54.75-59.1,144.51-74c4.1-.68,8.24-1.12,12.38-1.4C426.41,106.6,557.79,95.18,567.33,44.36Z" fill="#fff" opacity="0.3"/>
<rect x="31.14" y="128.09" width="187.86" height="213.62" fill="#787878" data-primary="true"/>
<rect x="31.14" y="128.09" width="187.86" height="34.35" fill="#fff" opacity="0.3"/>
<rect x="46.17" y="173.18" width="57.97" height="57.97" fill="#282728" data-secondary="true"/>
<circle cx="164.78" cy="145.27" r="3.76" fill="#fff" opacity="0.3"/>
<circle cx="184.11" cy="145.27" r="3.76" fill="#fff" opacity="0.3"/>
<circle cx="203.43" cy="145.27" r="3.76" fill="#fff" opacity="0.3"/>
<path d="M170.69,192.5H117a1.07,1.07,0,1,1,0-2.14h53.67a1.07,1.07,0,0,1,0,2.14Z" fill="#fff"/>
<path d="M186.25,205.38h-68.7a1.07,1.07,0,0,1,0-2.14h68.7a1.07,1.07,0,1,1,0,2.14Z" fill="#fff"/>
<path d="M203.43,218.27H117.55a1.08,1.08,0,0,1,0-2.15h85.88a1.08,1.08,0,0,1,0,2.15Z" fill="#fff"/>
<path d="M168,287H84.28a1.08,1.08,0,1,1,0-2.15H168a1.08,1.08,0,0,1,0,2.15Z" fill="#fff"/>
<path d="M194.84,299.85H57.44a1.08,1.08,0,1,1,0-2.15h137.4a1.08,1.08,0,1,1,0,2.15Z" fill="#fff"/>
<path d="M168.54,312.73H83.74a1.08,1.08,0,1,1,0-2.15h84.8a1.08,1.08,0,1,1,0,2.15Z" fill="#fff"/>
<rect x="83.74" y="248.32" width="78.36" height="16.1" fill="#fff" opacity="0.3"/>
<rect x="256.57" y="259.06" width="66.55" height="17.18" fill="#787878" opacity="0.29" data-primary="true"/>
<path d="M308.78,293.79H256.57a1.08,1.08,0,1,1,0-2.15h52.21a1.08,1.08,0,1,1,0,2.15Z" fill="#e6e6e6"/>
<path d="M325.8,306.67H256.57a1.07,1.07,0,1,1,0-2.14H325.8a1.07,1.07,0,1,1,0,2.14Z" fill="#e6e6e6"/>
<path d="M339.76,319.55H256.57a1.07,1.07,0,1,1,0-2.14h83.19a1.07,1.07,0,0,1,0,2.14Z" fill="#e6e6e6"/>
<path d="M379.48,332.44H256.57a1.08,1.08,0,1,1,0-2.15H379.48a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
<rect x="256.57" y="348.15" width="154.58" height="15.03" fill="#787878" opacity="0.29" data-primary="true"/>
<path d="M252.45,400.29h-122a1.08,1.08,0,0,1,0-2.15h122a1.08,1.08,0,1,1,0,2.15Z" fill="#e6e6e6"/>
<path d="M353.18,400.29H268.91a1.08,1.08,0,0,1,0-2.15h84.27a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
<path d="M417.59,400.29H388.06a1.08,1.08,0,0,1,0-2.15h29.53a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
<rect x="256.57" y="396.53" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
<rect x="360.69" y="396.53" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
<rect x="373.57" y="396.53" width="5.37" height="5.37" fill="#ccc"/>
<path d="M223.29,429.16H131a1.08,1.08,0,0,1,0-2.15h92.32a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
<path d="M289.84,455.37H129.9a1.08,1.08,0,1,1,0-2.15H289.84a1.08,1.08,0,1,1,0,2.15Z" fill="#e6e6e6"/>
<path d="M325.27,429.16H255a1.08,1.08,0,1,1,0-2.15h70.31a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
<path d="M349.42,455.37h-36a1.08,1.08,0,0,1,0-2.15h36a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
<rect x="227.58" y="425.4" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
<rect x="240.46" y="425.4" width="5.37" height="5.37" fill="#ccc"/>
<rect x="290.92" y="451.61" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
<rect x="303.8" y="451.61" width="5.37" height="5.37" fill="#ccc"/>
<path d="M355.32,512.93H298.43a1.08,1.08,0,0,1,0-2.15h56.89a1.08,1.08,0,1,1,0,2.15Z" fill="#e6e6e6"/>
<path d="M416,512.93H388.06a1.08,1.08,0,0,1,0-2.15H416a1.08,1.08,0,1,1,0,2.15Z" fill="#e6e6e6"/>
<rect x="361.77" y="509.17" width="5.37" height="5.37" fill="#ccc"/>
<rect x="374.65" y="509.17" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
<path d="M416,455.37H375.72a1.08,1.08,0,0,1,0-2.15H416a1.08,1.08,0,1,1,0,2.15Z" fill="#e6e6e6"/>
<rect x="353.18" y="451.61" width="5.37" height="5.37" fill="#ccc"/>
<rect x="366.06" y="451.61" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
<path d="M205,485H131a1.08,1.08,0,0,1,0-2.15H205a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
<path d="M349.42,485h-52.6a1.08,1.08,0,0,1,0-2.15h52.6a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
<path d="M416,485H363.38a1.08,1.08,0,1,1,0-2.15H416a1.08,1.08,0,1,1,0,2.15Z" fill="#e6e6e6"/>
<rect x="207.19" y="481.26" width="5.37" height="5.37" fill="#ccc"/>
<rect x="220.07" y="481.26" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
<rect x="231.88" y="481.26" width="5.37" height="5.37" fill="#ccc"/>
<path d="M256.57,512.93H131a1.08,1.08,0,0,1,0-2.15h125.6a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
<rect x="258.71" y="509.17" width="5.37" height="5.37" fill="#ccc"/>
<rect x="271.59" y="509.17" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
<rect x="283.4" y="509.17" width="5.37" height="5.37" fill="#ccc"/>
<rect x="244.76" y="481.26" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
<rect x="259.79" y="481.26" width="5.37" height="5.37" fill="#ccc"/>
<rect x="271.59" y="481.26" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
<rect x="284.48" y="481.26" width="5.37" height="5.37" fill="#ccc"/>
<path d="M417.59,429.16H358a1.08,1.08,0,1,1,0-2.15h59.58a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
<rect x="330.63" y="425.4" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
<rect x="343.52" y="425.4" width="5.37" height="5.37" fill="#ccc"/>
<rect x="51.53" y="436.18" width="103.05" height="64.41" fill="#787878" data-primary="true"/>
<g opacity="0.3">
<path d="M88.5,485.36a1.06,1.06,0,0,1-.74-.3l-15.5-14.83a1.06,1.06,0,0,1,0-1.54l15.49-15a1.07,1.07,0,0,1,1.52,0,1.08,1.08,0,0,1,0,1.52l-14.7,14.25,14.69,14.06a1.07,1.07,0,0,1,0,1.52A1.1,1.1,0,0,1,88.5,485.36Z" fill="#fff"/>
</g>
<g opacity="0.3">
<path d="M119.16,485.36a1.07,1.07,0,0,1-.74-1.84l14.69-14.26L118.42,455.2a1.07,1.07,0,0,1,1.48-1.55l15.5,14.83a1.07,1.07,0,0,1,.33.77,1.08,1.08,0,0,1-.32.78l-15.5,15A1.08,1.08,0,0,1,119.16,485.36Z" fill="#fff"/>
</g>
<g opacity="0.3">
<path d="M96.62,483.41a1.11,1.11,0,0,1-.5-.12,1.07,1.07,0,0,1-.45-1.45l14-26.83a1.08,1.08,0,1,1,1.91,1l-14,26.83A1.06,1.06,0,0,1,96.62,483.41Z" fill="#fff"/>
</g>
<rect x="434.76" y="367.48" width="11.81" height="208.25" fill="#999"/>
<rect x="441.2" y="367.48" width="5.37" height="208.25" opacity="0.1"/>
<rect x="471.26" y="368.01" width="11.81" height="172.29" fill="#999"/>
<rect x="477.7" y="368.01" width="5.37" height="172.29" opacity="0.1"/>
<rect x="728.89" y="367.48" width="11.81" height="208.25" fill="#999"/>
<rect x="735.33" y="367.48" width="5.37" height="208.25" opacity="0.1"/>
<rect x="758.95" y="354.06" width="11.81" height="186.25" fill="#999"/>
<rect x="765.39" y="354.06" width="5.37" height="186.25" opacity="0.1"/>
<path d="M688.1,271.94h40.53a4.55,4.55,0,0,1,4.55,4.55v67.37a0,0,0,0,1,0,0H688.1a0,0,0,0,1,0,0V271.94A0,0,0,0,1,688.1,271.94Z" fill="#b3b3b3"/>
<polygon points="421.88 364.26 477.27 336.37 786.88 336.37 750.36 364.26 421.88 364.26" fill="#ccc"/>
<path d="M542.11,559.63l-32.5,25.42S496,597.2,507.76,604.71c0,0,17.17,10.74,31.13-7.51l19.37-31.64Z" fill="#787878" data-primary="true"/>
<path d="M505.61,596.12c8,8.68,20.58,6.87,28.45-1,3.7-3.79,7-8.33,10.52-12.3,3.08-3.62,7.51-8.79,10.65-12.28-2.8,3.74-7.06,9.09-10,12.81-3.41,4.12-6.73,8.65-10.42,12.54-8.21,8.11-21.45,9.88-29.19.26Z" opacity="0.2"/>
<path d="M512.32,583.74c6.45-.09,13.31,2.42,17.35,7.63a15.61,15.61,0,0,1,2.79,5.84c-.26-.47-.51-1-.74-1.43a8.51,8.51,0,0,0-.81-1.37c-4-6.39-11.44-9.4-18.59-10.67Z" opacity="0.2"/>
<path d="M519.56,580c4.83-.65,11.72.93,12.9,6.4-2.62-4.61-8.1-5.41-12.9-6.4Z" opacity="0.2"/>
<path d="M523.86,575.73c4.82-.65,11.72.93,12.89,6.39-2.61-4.6-8.1-5.4-12.89-6.39Z" opacity="0.2"/>
<path d="M532.45,569.29c4.82-.65,11.72.93,12.89,6.39-2.61-4.61-8.1-5.4-12.89-6.39Z" opacity="0.2"/>
<path d="M550.16,544.06l-8,15.57s-3.32,4,1.25,6.48a8.52,8.52,0,0,0,4.06,1h7.9a3.61,3.61,0,0,0,2.94-1.51L568,551.93S554.41,546.7,550.16,544.06Z" fill="#f9b499"/>
<polygon points="548.32 510.23 551.84 520.86 557.18 505.66 548.32 510.23" fill="#f9b499"/>
<path d="M710.77,332.4c0,8.24-5.5,8.24-5.5,8.24l-15,5.37c-6.68,2.23-9.44,1.89-10.5,1.46a1.62,1.62,0,0,0-1.36.1.24.24,0,0,1-.08.06,11.71,11.71,0,0,1-3.82,1.75h0c-2.89.58-2.48-2.31-2.48-2.31a12.77,12.77,0,0,0,.2-1.54,9.91,9.91,0,0,0-5.8-9.37,26.59,26.59,0,0,0-4.77-1.68,6.38,6.38,0,0,0-3.07,0l-8.33,1.91H608.39L570,335.27a98.24,98.24,0,0,1,12.58-65.48,86.2,86.2,0,0,1,8.82-12.47c.72-.84,1.14-1.27,1.14-1.27l2.37-3.43s32,2.14,32.37,1.07,22.15-14,22.15-14l30.76,13a10.63,10.63,0,0,1,3.59,2.59c6.62,6.85,11.81,23.17,11.81,23.17l14,46.16A30.89,30.89,0,0,1,710.77,332.4Z" fill="#787878" data-primary="true"/>
<path d="M675.8,305s-30.74,5-53.75.22c-.59-.12-1.17-.27-1.75-.43A88.92,88.92,0,0,0,592.56,302l-22.06-.18-1.09,7.3h36.87s12,.39,21.7,3.61c0,0,9.66,3.22,29,1.07l21.82-2.66Z" fill="#282728" data-secondary="true"/>
<path d="M683.8,255.21c-20.39,2.6-56.89,14.58-56.89,14.58-8.59-6.44-35.49-12.47-35.49-12.47.72-.84,1.14-1.27,1.14-1.27l2.37-3.43s32,2.14,32.37,1.07,22.15-14,22.15-14l30.76,13A10.63,10.63,0,0,1,683.8,255.21Z" opacity="0.2"/>
<path d="M620.1,254.32a12.38,12.38,0,0,1-1.24.26c-7.26,1.28-14.75-1.87-20.74-8a43,43,0,0,1-10.73-19.86c-4.59-18.58,2.63-36.33,16.12-39.66s28.13,9,32.72,27.59S633.6,251,620.1,254.32Z" fill="#f9b499"/>
<ellipse cx="639.26" cy="215.05" rx="1.61" ry="3.22" fill="none" stroke="red" stroke-miterlimit="10" stroke-width="0.75"/>
<path d="M651.6,210.75s17.18,9.45-10.73,18.14a15.44,15.44,0,0,1-.54-3.65,15.8,15.8,0,0,1,.54-4.54l-8.59-4.36s-4.67.85-7.17-4.52c0,0-8.93,0-7.86-6.44,0,0-6.44,4.3-8.59-1.07,0,0-9.69,6.88-19.33-4.62,0,0-4.28,7.84-3,21,0,0-6.34,5.08-9.93.38a6.63,6.63,0,0,1-1.28-3.77,5.58,5.58,0,0,1,3.22-5.49s-7.77-2.89-7.56-9.28a10.2,10.2,0,0,1,1.41-4.67s1.61-4,7.63-2.31h0a19.17,19.17,0,0,1,3.1,1.24s-8.21-17.26,3.4-28.49c0,0,19.14-19.82,26.66,4.87,0,0,6.55-10.14,17-7.62h0a15.76,15.76,0,0,1,2.25.72s7.51,2,6.44,14.1c0,0,9.66-8.28,18.25,1.38,0,0,6.44,7.89,0,16.28C657,198.05,660.19,205.38,651.6,210.75Z" fill="#282728" data-secondary="true"/>
<path d="M590.41,197.6s-3.22-10.46,6.44-16.91c0,0,6.93-4.51,16.49,1.46a23.89,23.89,0,0,1,2.73,2.07,16.44,16.44,0,0,0,10.59,4.11s11-.47,12.6,12c0,0-12.35-10.72-21.47-5.54,0,0-4.83-15.22-17.72-10.93C600.07,183.91,592.56,186.06,590.41,197.6Z" opacity="0.2"/>
<path d="M579.79,195.56c-5.23.93-9,7-9,7a10.2,10.2,0,0,1,1.41-4.67S573.77,193.84,579.79,195.56Z" opacity="0.2"/>
<path d="M651.6,210.75s17.18,9.45-10.73,18.14a7.4,7.4,0,0,1-.54-3.65,7.26,7.26,0,0,1,.54-2.07l4.29-6.83h0c3.22-1.93,3.22-6.66,3.22-6.66a10.45,10.45,0,0,0,4.63-3.63,8.74,8.74,0,0,0,1.09-8.24c-2.23-5.68-8.94-4.09-8.94-4.09,2.15-16-7.52-10.85-7.52-10.85-1-14.52-7.22-17.15-7.61-17.3a15.76,15.76,0,0,1,2.25.72s7.51,2,6.44,14.1c0,0,9.66-8.28,18.25,1.38,0,0,6.44,7.89,0,16.28C657,198.05,660.19,205.38,651.6,210.75Z" opacity="0.2"/>
<path d="M659.11,241.88l-26.83,22.77c-4.3,3.45-6.44.85-6.44.85l-1.19-1.87-5.79-9c20.4-4.65,18.67-31.41,18.67-31.41h3.34a17.63,17.63,0,0,0,2.14,11.2c3.91-1.57,9-1.42,11.4-1.23a14.8,14.8,0,0,1,2.89.49C665.93,236,659.11,241.88,659.11,241.88Z" fill="#f9b499"/>
<path d="M659.11,241.88l-26.83,22.77c-4.3,3.45-6.44.85-6.44.85l-1.19-1.87c11.69-5.09,23-18.83,23-18.83,4.06-6-2.64-9.53-4.26-10.28a.16.16,0,0,1,0-.29c3.86-1.42,8.68-1.27,11-1.09a14.8,14.8,0,0,1,2.89.49C665.93,236,659.11,241.88,659.11,241.88Z" fill="#f7a48b"/>
<path d="M618.86,254.58l3.07,4.81s18.66-10.53,15-26.36C637,233,635.5,251.18,618.86,254.58Z" fill="#f7a48b"/>
<path d="M599,253.69a55.57,55.57,0,0,1,18.79,6.51" fill="none" stroke="red" stroke-miterlimit="10" stroke-width="0.75"/>
<path d="M710.77,332.4c0,8.24-5.5,8.24-5.5,8.24l-15,5.37c-6.68,2.23-9.44,1.89-10.5,1.46a1.62,1.62,0,0,0-1.36.1c.36-.24,1.68-1.46-.08-5l-5.63-8.42a1.13,1.13,0,0,1,.39-1.6,1.07,1.07,0,0,1,.55-.14,1.12,1.12,0,0,1,.91.46l7.14,9.93s1.07,4.29,15-2.15C696.69,340.64,708.75,336,710.77,332.4Z" opacity="0.2"/>
<path d="M674.48,349.38h0c-2.89.58-2.48-2.31-2.48-2.31a12.77,12.77,0,0,0,.2-1.54,9.91,9.91,0,0,0-5.8-9.37,26.59,26.59,0,0,0-4.77-1.68s8.22-3.51,12.51,7.22C674.14,341.71,675.46,346.35,674.48,349.38Z" opacity="0.2"/>
<path d="M677.36,323.46s-14,5.89-18.8,11l-8.33,1.91H608.39L570,335.27a98.24,98.24,0,0,1,12.58-65.48c.3,0,8.89,2.15,17.47,32.21,0,0,8.53,31.81,22,30.4h23.5s21.08.73,21.08-13.23V297.7s.65-8.42,4.09-3.13L681.66,317S683.8,321.32,677.36,323.46Z" opacity="0.2"/>
<path d="M680.58,258c-8.42,6.71-12.77,17.28-12.88,27.91-.1-1.33-.27-2.68-.25-4,0-9.45,4.89-19.05,13.13-23.89Z" opacity="0.2"/>
<path d="M640.87,324h0a.54.54,0,0,1-.52-.55l1.07-32.21c0-21.29,5.35-36.51,5.4-36.66a.54.54,0,0,1,.69-.32.53.53,0,0,1,.32.68c0,.15-5.33,15.2-5.33,36.32l-1.08,32.22A.54.54,0,0,1,640.87,324Z" fill="#282728" data-secondary="true"/>
<path d="M614,327.22h0a.54.54,0,0,1-.52-.55l1.08-31.13a208.17,208.17,0,0,1,2.69-33.9.53.53,0,0,1,.62-.43.54.54,0,0,1,.43.63,208.45,208.45,0,0,0-2.67,33.71l-1.07,31.15A.55.55,0,0,1,614,327.22Z" fill="#282728" data-secondary="true"/>
<g opacity="0.2">
<path d="M640.87,324h0a.54.54,0,0,1-.52-.55l1.07-32.21c0-21.29,5.35-36.51,5.4-36.66a.54.54,0,0,1,.69-.32.53.53,0,0,1,.32.68c0,.15-5.33,15.2-5.33,36.32l-1.08,32.22A.54.54,0,0,1,640.87,324Z"/>
<path d="M614,327.22h0a.54.54,0,0,1-.52-.55l1.08-31.13a208.17,208.17,0,0,1,2.69-33.9.53.53,0,0,1,.62-.43.54.54,0,0,1,.43.63,208.45,208.45,0,0,0-2.67,33.71l-1.07,31.15A.55.55,0,0,1,614,327.22Z"/>
</g>
<path d="M706.34,371.77c0,.17,0,8.76-2.14,25.76l-.34,2.43a58.67,58.67,0,0,0-.52,7.27c-.06,3.84-2.56,11.15-21.3,8.21.49-.83.94-1.65,1.36-2.49l.35-.68c1.57-3,3.69-7.58,4.35-11.52,0,0-59-5.36-78.37-12.88,0,0-28.3-11.81-38.84-8.05l-8.39,7s-7.51-2.15,3.22-15Z" fill="#787878" data-primary="true"/>
<path d="M688.1,400.75s13.8.92,13.29,7a4,4,0,0,1-1.61,2.85c-1.84,1.41-6.25,3.29-16.15,1.89A63,63,0,0,0,688.1,400.75Z" opacity="0.2"/>
<path d="M564.14,385.44s-6.33-4,6.75-5.62Z" opacity="0.2"/>
<path d="M688.1,400.75a53.84,53.84,0,0,1-4.35,11.52l-.35.68c-.23.43-.44.81-.63,1.15s-.47.89-.73,1.34c-13.3,23.42-41.56,44.44-53.06,52.37a12.19,12.19,0,0,0-5.27,9.22c-2.38,36.84-19.34,68.64-19.34,68.64-3.61,5.17-9,7.66-15,8.46-19.1,2.58-45.12-11.68-45.12-11.68l14-39.72c11.81-44,23.61-56.89,23.61-56.89l26.54-55.12,1.37-2.85C629.06,395.39,688.1,400.75,688.1,400.75Z" fill="#282728" data-secondary="true"/>
<path d="M688.1,400.75a53.84,53.84,0,0,1-4.35,11.52l-.35.68c-.23.43-.44.81-.63,1.15s-.47.89-.73,1.34c-13.3,23.42-41.56,44.44-53.06,52.37a12.19,12.19,0,0,0-5.27,9.22c-2.38,36.84-19.34,68.64-19.34,68.64-3.61,5.17-9,7.66-15,8.46,20-25.72,31.27-83,31.27-83-.89-13.33,38.47-47.86,38.47-47.86,15-15-4.29-19.33-4.29-19.33-17.27-2.72-39.92-10.84-46.46-13.25l1.37-2.85C629.06,395.39,688.1,400.75,688.1,400.75Z" opacity="0.2"/>
<path d="M589.37,430.17l-33.31,13.52,13,26s-5.54,16.19-11.4,35.76c0,0-13.42,9.12-27.37,8.05,0,0-11.81-30.06-18.25-61.19,0,0-8.59-18.25,6.44-30.06L562,387.05l8.93-7.23s6.64-4.83,38.84,8.05Z" fill="#282728" data-secondary="true"/>
<path d="M547.62,532.88,526.35,549.3a27.56,27.56,0,0,1-6.81,3.93c-3.21,1.25-7.85,4-6.84,9.17a8.45,8.45,0,0,0,5.07,6.09c2.55,1.06,6.59,1.78,12.53.37l11.81-9.23,7.47-14.54-5.33-2.64Z" fill="#787878" data-primary="true"/>
<path d="M547.62,532.88,526.35,549.3a27.56,27.56,0,0,1-6.81,3.93c-3.21,1.25-7.85,4-6.84,9.17a8.45,8.45,0,0,0,5.07,6.09c2.55,1.06,6.59,1.78,12.53.37l11.81-9.23,7.47-14.54-5.33-2.64Z" opacity="0.2"/>
<path d="M589.37,430.17l-33.31,13.52,13,26a370.36,370.36,0,0,1-11.89,36s-12.93,8.88-26.88,7.81c39.72-3.22,20.78-67.22,20.78-67.22-4.38-9.78,2.19-12.65,6.18-13.48a53.87,53.87,0,0,0,7.5-2.12l10.06-3.7a29.52,29.52,0,0,0,16.91-15.53,27.2,27.2,0,0,0,2.23-8.31,14.25,14.25,0,0,0-10.3-15c-8.42-2.48-16.86-1.84-21.7-1.1l8.93-7.23s6.64-4.83,38.84,8.05Z" opacity="0.2"/>
<path d="M512.56,561c8,4.49,17.84,3.92,26,.25,1.19-.51,2.33-1.14,3.54-1.66-1.08.73-2.18,1.45-3.3,2.13-8,4.53-18.78,5.1-26.25-.72Z" opacity="0.2"/>
<path d="M524.65,550.52c4.14,1.84,12.18,6.84,12.1,11.9-.72-2.81-3.3-4.59-5.35-6.46s-4.5-3.57-6.75-5.44Z" opacity="0.2"/>
<path d="M530.3,546.25c3.1.44,5.52,3.24,6.45,6.1-2.23-2.05-4-4.27-6.45-6.1Z" opacity="0.2"/>
<path d="M534.32,543.14a9.55,9.55,0,0,1,6.62,6.16c-2.17-2.19-4.35-4-6.62-6.16Z" opacity="0.2"/>
<path d="M538.89,539.61a11.81,11.81,0,0,1,5.36,6.61,29.31,29.31,0,0,1-5.36-6.61Z" opacity="0.2"/>
<rect x="421.88" y="364.26" width="328.48" height="7.51" fill="#b3b3b3"/>
<polygon points="750.36 364.26 750.36 371.77 786.86 342.79 786.88 336.37 750.36 364.26" fill="#999"/>
<path d="M507.76,344.93h98.07l-7.33-63.74a5.61,5.61,0,0,0-5.57-5h-90a3,3,0,0,0-2.93,3.31Z" fill="#787878" data-primary="true"/>
<path d="M605.83,344.93H507.76L500,279.54a3,3,0,0,1,2.95-3.31h90a5.61,5.61,0,0,1,5.56,5Z" fill="#fff" opacity="0.3"/>
<polygon points="583.53 276.23 507.76 341.71 506.12 329.04 567.52 276.23 583.53 276.23" fill="#fff" opacity="0.3"/>
<path d="M517.07,344.93l79.55-67.31a6,6,0,0,1,1.88,3.57l.38,3.34-71.09,60.4Z" fill="#fff" opacity="0.3"/>
<rect x="507.76" y="344.93" width="94.46" height="6.44" fill="#787878" data-primary="true"/>
<rect x="602.22" y="344.93" width="29.49" height="6.44" fill="#787878" data-primary="true"/>
<rect x="602.22" y="344.93" width="29.49" height="6.44" opacity="0.2"/>
<polygon points="419.73 353.52 466.38 353.52 499.97 333.94 459.85 333.94 419.73 353.52" fill="#fff"/>
<rect x="419.73" y="353.52" width="46.65" height="4.65" fill="#e6e6e6"/>
<polygon points="499.97 333.94 499.97 339.8 466.38 358.17 466.38 353.52 499.97 333.94" fill="#ccc"/>
<polygon points="499.97 333.94 499.97 339.8 466.38 358.17 466.38 353.52 499.97 333.94" opacity="0.1"/>
<path d="M658.56,334.46s-13.47,1.87-20.95,12.08c0,0-10.05,9.15-.18,7.53,0,0,.47,4.68,8.39,1.53,0,0,1.37,3.31,10-1.53,0,0,8.64-4.84,16.16-7C672,347.08,675.17,334.71,658.56,334.46Z" fill="#f9b499"/>
<path d="M646,343.86a40.12,40.12,0,0,1-8.55,10.21A40.49,40.49,0,0,1,646,343.86Z" fill="#f7a48b"/>
<path d="M645.82,355.6a24.61,24.61,0,0,1,6.85-7.82,24.71,24.71,0,0,1-6.85,7.82Z" fill="#f7a48b"/>
<ellipse cx="638.72" cy="215.58" rx="6.44" ry="8.05" fill="#f9b499"/>
<path d="M640.87,228.89s12.2-4.93,24.24-3.72a26.56,26.56,0,0,1,17.33,9.17c4.85,5.6,11.54,15.1,4.38,18.3a8.59,8.59,0,0,1-7.29-.33c-5-2.49-17.91-6.91-47.79,12.65l27.37-23.08s6.49-6.48-3.5-8.53a13.52,13.52,0,0,0-2.62-.25,46.27,46.27,0,0,0-10,1.27S640.87,230.93,640.87,228.89Z" fill="#787878" data-primary="true"/>
<path d="M640.87,228.89s12.2-4.93,24.24-3.72a26.56,26.56,0,0,1,17.33,9.17c4.85,5.6,11.54,15.1,4.38,18.3a8.59,8.59,0,0,1-7.29-.33c-5-2.49-17.91-6.91-47.79,12.65l27.37-23.08s6.49-6.48-3.5-8.53a13.52,13.52,0,0,0-2.62-.25,46.27,46.27,0,0,0-10,1.27S640.87,230.93,640.87,228.89Z" fill="#fff" opacity="0.3"/>
<path d="M674.14,234.37c-5.73,6.95-13.48,12.06-21.25,16.49-1.15.58-2.28,1.2-3.44,1.76,8.36-5.92,17-11.41,24.69-18.25Z" fill="#fff" opacity="0.3"/>
<path d="M683.8,238.66C679,244,671.85,246.84,664.89,248c6.47-2.57,13.26-5.24,18.91-9.35Z" fill="#fff" opacity="0.3"/>
<path d="M625.84,265.5c-4.44-2.67-21.36-6.8-27.08-8.15a23.81,23.81,0,0,0-3.37-.5c-4.81-.45-3.9-3.16-3.9-3.16,0-4.29,6.63-7.09,6.63-7.09,6,6.11,13.48,9.26,20.74,8Z" fill="#787878" data-primary="true"/>
<path d="M625.84,265.5c-4.44-2.67-21.36-6.8-27.08-8.15a23.81,23.81,0,0,0-3.37-.5c-4.81-.45-3.9-3.16-3.9-3.16,0-4.29,6.63-7.09,6.63-7.09,6,6.11,13.48,9.26,20.74,8Z" fill="#fff" opacity="0.3"/>
<circle cx="551.23" cy="311.12" r="8.05" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 23 KiB

9
public/favicon.svg Normal file
View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #FFF; }
}
</style>
</svg>

After

Width:  |  Height:  |  Size: 749 B

76
public/toggle-theme.js Normal file
View File

@ -0,0 +1,76 @@
const primaryColorScheme = ""; // "light" | "dark"
// Get theme data from local storage
const currentTheme = localStorage.getItem("theme");
function getPreferTheme() {
// return theme value in local storage if it is set
if (currentTheme) return currentTheme;
// return primary color scheme if it is set
if (primaryColorScheme) return primaryColorScheme;
// return user device's prefer color scheme
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
let themeValue = getPreferTheme();
function setPreference() {
localStorage.setItem("theme", themeValue);
reflectPreference();
}
function reflectPreference() {
document.firstElementChild.setAttribute("data-theme", themeValue);
document.querySelector("#theme-btn")?.setAttribute("aria-label", themeValue);
// Get a reference to the body element
const body = document.body;
// Check if the body element exists before using getComputedStyle
if (body) {
// Get the computed styles for the body element
const computedStyles = window.getComputedStyle(body);
// Get the background color property
const bgColor = computedStyles.backgroundColor;
// Set the background color in <meta theme-color ... />
document
.querySelector("meta[name='theme-color']")
?.setAttribute("content", bgColor);
}
}
// set early so no page flashes / CSS is made aware
reflectPreference();
window.onload = () => {
function setThemeFeature() {
// set on load so screen readers can get the latest value on the button
reflectPreference();
// now this script can find and listen for clicks on the control
document.querySelector("#theme-btn")?.addEventListener("click", () => {
themeValue = themeValue === "light" ? "dark" : "light";
setPreference();
});
}
setThemeFeature();
// Runs on view transitions navigation
document.addEventListener("astro:after-swap", setThemeFeature);
};
// sync with system changes
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", ({ matches: isDark }) => {
themeValue = isDark ? "dark" : "light";
setPreference();
});

1
remark-collapse.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module 'remark-collapse';

View File

@ -0,0 +1 @@
<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" class="icon icon-tabler icons-tabler-outline icon-tabler-archive"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 4m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v0a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z" /><path d="M5 8v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-10" /><path d="M10 12l4 0" /></svg>

After

Width:  |  Height:  |  Size: 484 B

View File

@ -0,0 +1 @@
<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" class="icon icon-tabler icons-tabler-outline icon-tabler-arrow-left"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 12l14 0" /><path d="M5 12l6 6" /><path d="M5 12l6 -6" /></svg>

After

Width:  |  Height:  |  Size: 385 B

View File

@ -0,0 +1 @@
<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" class="icon icon-tabler icons-tabler-outline icon-tabler-arrow-right"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 12l14 0" /><path d="M13 18l6 -6" /><path d="M13 6l6 6" /></svg>

After

Width:  |  Height:  |  Size: 387 B

View File

@ -0,0 +1 @@
<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" class="icon icon-tabler icons-tabler-outline icon-tabler-brand-x"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 4l11.733 16h4.267l-11.733 -16z" /><path d="M4 20l6.768 -6.768m2.46 -2.46l6.772 -6.772" /></svg>

After

Width:  |  Height:  |  Size: 415 B

View File

@ -0,0 +1 @@
<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" class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-left"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M15 6l-6 6l6 6" /></svg>

After

Width:  |  Height:  |  Size: 346 B

View File

@ -0,0 +1 @@
<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" class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-right"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 6l6 6l-6 6" /></svg>

After

Width:  |  Height:  |  Size: 346 B

View File

@ -0,0 +1 @@
<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" class="icon icon-tabler icons-tabler-outline icon-tabler-edit"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1" /><path d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415z" /><path d="M16 5l3 3" /></svg>

After

Width:  |  Height:  |  Size: 487 B

View File

@ -0,0 +1 @@
<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" class="icon icon-tabler icons-tabler-outline icon-tabler-brand-facebook"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M7 10v4h3v7h4v-7h3l1 -4h-4v-2a1 1 0 0 1 1 -1h3v-4h-3a5 5 0 0 0 -5 5v2h-3" /></svg>

After

Width:  |  Height:  |  Size: 406 B

View File

@ -0,0 +1 @@
<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" class="icon icon-tabler icons-tabler-outline icon-tabler-brand-github"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 19c-4.3 1.4 -4.3 -2.5 -6 -3m12 5v-3.5c0 -1 .1 -1.4 -.5 -2c2.8 -.3 5.5 -1.4 5.5 -6a4.6 4.6 0 0 0 -1.3 -3.2a4.2 4.2 0 0 0 -.1 -3.2s-1.1 -.3 -3.5 1.3a12.3 12.3 0 0 0 -6.2 0c-2.4 -1.6 -3.5 -1.3 -3.5 -1.3a4.2 4.2 0 0 0 -.1 3.2a4.6 4.6 0 0 0 -1.3 3.2c0 4.6 2.7 5.7 5.5 6c-.6 .6 -.6 1.2 -.5 2v3.5" /></svg>

After

Width:  |  Height:  |  Size: 624 B

View File

@ -0,0 +1 @@
<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" class="icon icon-tabler icons-tabler-outline icon-tabler-hash"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 9l14 0" /><path d="M5 15l14 0" /><path d="M11 4l-4 16" /><path d="M17 4l-4 16" /></svg>

After

Width:  |  Height:  |  Size: 404 B

View File

@ -0,0 +1 @@
<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" class="icon icon-tabler icons-tabler-outline icon-tabler-brand-linkedin"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M8 11v5" /><path d="M8 8v.01" /><path d="M12 16v-5" /><path d="M16 16v-3a2 2 0 1 0 -4 0" /><path d="M3 7a4 4 0 0 1 4 -4h10a4 4 0 0 1 4 4v10a4 4 0 0 1 -4 4h-10a4 4 0 0 1 -4 -4z" /></svg>

After

Width:  |  Height:  |  Size: 509 B

View File

@ -0,0 +1 @@
<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" class="icon icon-tabler icons-tabler-outline icon-tabler-mail"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 7a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-10z" /><path d="M3 7l9 6l9 -6" /></svg>

After

Width:  |  Height:  |  Size: 429 B

View File

@ -0,0 +1 @@
<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" class="icon icon-tabler icons-tabler-outline icon-tabler-menu-deep"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 6h16" /><path d="M7 12h13" /><path d="M10 18h10" /></svg>

After

Width:  |  Height:  |  Size: 379 B

View File

@ -0,0 +1 @@
<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" class="icon icon-tabler icons-tabler-outline icon-tabler-moon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z" /></svg>

After

Width:  |  Height:  |  Size: 402 B

View File

@ -0,0 +1 @@
<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" class="icon icon-tabler icons-tabler-outline icon-tabler-brand-pinterest"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M8 20l4 -9" /><path d="M10.7 14c.437 1.263 1.43 2 2.55 2c2.071 0 3.75 -1.554 3.75 -4a5 5 0 1 0 -9.7 1.7" /><path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" /></svg>

After

Width:  |  Height:  |  Size: 493 B

View File

@ -0,0 +1 @@
<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" class="icon icon-tabler icons-tabler-outline icon-tabler-rss"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M4 4a16 16 0 0 1 16 16" /><path d="M4 11a9 9 0 0 1 9 9" /></svg>

After

Width:  |  Height:  |  Size: 429 B

View File

@ -0,0 +1 @@
<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" class="icon icon-tabler icons-tabler-outline icon-tabler-search"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" /><path d="M21 21l-6 -6" /></svg>

After

Width:  |  Height:  |  Size: 393 B

View File

@ -0,0 +1 @@
<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" class="icon icon-tabler icons-tabler-outline icon-tabler-sun"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0" /><path d="M3 12h1m8 -9v1m8 8h1m-9 8v1m-6.4 -15.4l.7 .7m12.1 -.7l-.7 .7m0 11.4l.7 .7m-12.1 -.7l-.7 .7" /></svg>

After

Width:  |  Height:  |  Size: 466 B

View File

@ -0,0 +1 @@
<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" class="icon icon-tabler icons-tabler-outline icon-tabler-sun-high"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M14.828 14.828a4 4 0 1 0 -5.656 -5.656a4 4 0 0 0 5.656 5.656z" /><path d="M6.343 17.657l-1.414 1.414" /><path d="M6.343 6.343l-1.414 -1.414" /><path d="M17.657 6.343l1.414 -1.414" /><path d="M17.657 17.657l1.414 1.414" /><path d="M4 12h-2" /><path d="M12 4v-2" /><path d="M20 12h2" /><path d="M12 20v2" /></svg>

After

Width:  |  Height:  |  Size: 629 B

View File

@ -0,0 +1 @@
<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" class="icon icon-tabler icons-tabler-outline icon-tabler-brand-telegram"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M15 10l-4 4l6 6l4 -16l-18 7l4 2l2 6l3 -4" /></svg>

After

Width:  |  Height:  |  Size: 374 B

View File

@ -0,0 +1 @@
<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" class="icon icon-tabler icons-tabler-outline icon-tabler-brand-whatsapp"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 21l1.65 -3.8a9 9 0 1 1 3.4 2.9l-5.05 .9" /><path d="M9 10a.5 .5 0 0 0 1 0v-1a.5 .5 0 0 0 -1 0v1a5 5 0 0 0 5 5h1a.5 .5 0 0 0 0 -1h-1a.5 .5 0 0 0 0 1" /></svg>

After

Width:  |  Height:  |  Size: 484 B

View File

@ -0,0 +1 @@
<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" class="icon icon-tabler icons-tabler-outline icon-tabler-x"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M18 6l-12 12" /><path d="M6 6l12 12" /></svg>

After

Width:  |  Height:  |  Size: 356 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

View File

@ -0,0 +1,37 @@
---
import IconChevronLeft from "@/assets/icons/IconChevronLeft.svg";
import LinkButton from "./LinkButton.astro";
import { SITE } from "@/config";
---
{
SITE.showBackButton && (
<div class="mx-auto flex w-full max-w-3xl items-center justify-start px-2">
<LinkButton
id="back-button"
href="/"
class="focus-outline mt-8 mb-2 flex hover:text-foreground/75"
>
<IconChevronLeft class="inline-block size-6" />
<span>返回</span>
</LinkButton>
</div>
)
}
<script>
/* Update Search Praam */
function updateGoBackUrl() {
const backButton: HTMLAnchorElement | null =
document.querySelector("#back-button");
const backUrl = sessionStorage.getItem("backUrl");
if (backUrl && backButton) {
backButton.href = backUrl;
}
}
document.addEventListener("astro:page-load", updateGoBackUrl);
updateGoBackUrl();
</script>

View File

@ -0,0 +1,71 @@
---
// Remove current url path and remove trailing slash if exists
const currentUrlPath = Astro.url.pathname.replace(/\/+$/, "");
// Get url array from path
// eg: /tags/tailwindcss => ['tags', 'tailwindcss']
const breadcrumbList = currentUrlPath.split("/").slice(1);
// if breadcrumb is Home > Posts > 1 <etc>
// replace Posts with Posts (page number)
if (breadcrumbList[0] === "posts") {
breadcrumbList.splice(0, 2, `文章 (页面 ${breadcrumbList[1] || 1})`);
}
// if breadcrumb is Home > Tags > [tag] > [page] <etc>
// replace [tag] > [page] with [tag] (page number)
if (breadcrumbList[0] === "tags" && !isNaN(Number(breadcrumbList[2]))) {
breadcrumbList.splice(
1,
3,
`${breadcrumbList[1]} ${Number(breadcrumbList[2]) === 1 ? "" : "(页面 " + breadcrumbList[2] + ")"}`
);
}
switch (breadcrumbList[0]) {
case "tags":
breadcrumbList[0] = "标签";
break;
case "about":
breadcrumbList[0] = "关于";
break;
case "archives":
breadcrumbList[0] = "归档";
break;
case "search":
breadcrumbList[0] = "搜索";
break;
}
---
<nav class="mx-auto mt-8 mb-1 w-full max-w-3xl px-4" aria-label="breadcrumb">
<ul
class="font-light [&>li]:inline [&>li:not(:last-child)>a]:hover:opacity-100"
>
<li>
<a href="/" class="opacity-80">首页</a>
<span aria-hidden="true" class="opacity-80">&raquo;</span>
</li>
{
breadcrumbList.map((breadcrumb, index) =>
index + 1 === breadcrumbList.length ? (
<li>
<span
class:list={["capitalize opacity-75", { lowercase: index > 0 }]}
aria-current="page"
>
{/* make the last part lowercase in Home > Tags > some-tag */}
{decodeURIComponent(breadcrumb)}
</span>
</li>
) : (
<li>
<a href={`/${breadcrumb}/`} class="capitalize opacity-70">
{breadcrumb}
</a>
<span aria-hidden="true">&raquo;</span>
</li>
)
)
}
</ul>
</nav>

37
src/components/Card.astro Normal file
View File

@ -0,0 +1,37 @@
---
import { slugifyStr } from "@/utils/slugify";
import type { CollectionEntry } from "astro:content";
import Datetime from "./Datetime.astro";
export interface Props {
href?: string;
frontmatter: CollectionEntry<"blog">["data"];
secHeading?: boolean;
}
const { href, frontmatter, secHeading = true } = Astro.props;
const { title, pubDatetime, modDatetime, description } = frontmatter;
const headerProps = {
style: { viewTransitionName: slugifyStr(title) },
class: "text-lg font-medium decoration-dashed hover:underline",
};
---
<li class="my-6">
<a
href={href}
class="inline-block text-lg font-medium text-accent decoration-dashed underline-offset-4 focus-visible:no-underline focus-visible:underline-offset-0"
>
{
secHeading ? (
<h2 {...headerProps}>{title}</h2>
) : (
<h3 {...headerProps}>{title}</h3>
)
}
</a>
<Datetime pubDatetime={pubDatetime} modDatetime={modDatetime} />
<p>{description}</p>
</li>

View File

@ -0,0 +1,66 @@
---
import { LOCALE } from "@/constants";
export interface Props {
pubDatetime: string | Date;
modDatetime: string | Date | undefined | null;
size?: "sm" | "lg";
class?: string;
}
const {
pubDatetime,
modDatetime,
size = "sm",
class: className = "",
} = Astro.props;
/* ========== Formatted Datetime ========== */
const myDatetime = new Date(
modDatetime && modDatetime > pubDatetime ? modDatetime : pubDatetime
);
const date = myDatetime.toLocaleDateString(LOCALE.langTag, {
year: "numeric",
month: "short",
day: "numeric",
});
const time = myDatetime.toLocaleTimeString(LOCALE.langTag, {
hour: "2-digit",
minute: "2-digit",
});
---
<div class={`flex items-end space-x-2 opacity-80 ${className}`.trim()}>
<svg
xmlns="http://www.w3.org/2000/svg"
class={`${
size === "sm" ? "scale-90" : "scale-100"
} inline-block h-6 w-6 min-w-[1.375rem] fill-foreground`}
aria-hidden="true"
>
<path
d="M7 11h2v2H7zm0 4h2v2H7zm4-4h2v2h-2zm0 4h2v2h-2zm4-4h2v2h-2zm0 4h2v2h-2z"
></path>
<path
d="M5 22h14c1.103 0 2-.897 2-2V6c0-1.103-.897-2-2-2h-2V2h-2v2H9V2H7v2H5c-1.103 0-2 .897-2 2v14c0 1.103.897 2 2 2zM19 8l.001 12H5V8h14z"
></path>
</svg>
{
modDatetime && modDatetime > pubDatetime ? (
<span
class={`italic ${size === "sm" ? "text-sm" : "text-sm sm:text-base"}`}
>
Updated:
</span>
) : (
<span class="sr-only">Published:</span>
)
}
<span class={`italic ${size === "sm" ? "text-sm" : "text-sm sm:text-base"}`}>
<time datetime={myDatetime.toISOString()}>{date}</time>
<span aria-hidden="true"> | </span>
<span class="sr-only">&nbsp;at&nbsp;</span>
<span class="text-nowrap">{time}</span>
</span>
</div>

View File

@ -0,0 +1,41 @@
---
import type { CollectionEntry } from "astro:content";
import IconEdit from "@/assets/icons/IconEdit.svg";
import { SITE } from "@/config";
export interface Props {
editPost?: CollectionEntry<"blog">["data"]["editPost"];
postId?: CollectionEntry<"blog">["id"];
class?: string;
}
const { editPost, postId, class: className = "" } = Astro.props;
let editPostUrl = editPost?.url ?? SITE?.editPost?.url ?? "";
const showEditPost = !editPost?.disabled && editPostUrl.length > 0;
const appendFilePath =
editPost?.appendFilePath ?? SITE?.editPost?.appendFilePath ?? false;
if (appendFilePath && postId) {
editPostUrl += `/${postId}`;
}
const editPostText = editPost?.text ?? SITE?.editPost?.text ?? "Edit";
---
{
showEditPost && (
<div class:list={["opacity-80", className]}>
<span aria-hidden="true" class="max-sm:hidden">
|
</span>
<a
class="space-x-1.5 hover:opacity-75"
href={editPostUrl}
rel="noopener noreferrer"
target="_blank"
>
<IconEdit class="inline-block size-6" />
<span class="italic max-sm:text-sm sm:inline">{editPostText}</span>
</a>
</div>
)
}

View File

@ -0,0 +1,26 @@
---
import Hr from "./Hr.astro";
import Socials from "./Socials.astro";
const currentYear = new Date().getFullYear();
export interface Props {
noMarginTop?: boolean;
}
const { noMarginTop = false } = Astro.props;
---
<footer class:list={["w-full", { "mt-auto": !noMarginTop }]}>
<Hr noPadding />
<div
class="flex flex-col items-center justify-between py-6 sm:flex-row-reverse sm:py-4"
>
<Socials centered />
<div class="my-2 flex flex-col items-center whitespace-nowrap sm:flex-row">
<span>Copyright &#169; {currentYear}</span>
<span class="hidden sm:inline">&nbsp;|&nbsp;</span>
<span>All rights reserved.</span>
</div>
</div>
</footer>

168
src/components/Header.astro Normal file
View File

@ -0,0 +1,168 @@
---
import Hr from "./Hr.astro";
import IconX from "@/assets/icons/IconX.svg";
import IconMoon from "@/assets/icons/IconMoon.svg";
import IconSearch from "@/assets/icons/IconSearch.svg";
import IconArchive from "@/assets/icons/IconArchive.svg";
import IconSunHigh from "@/assets/icons/IconSunHigh.svg";
import IconMenuDeep from "@/assets/icons/IconMenuDeep.svg";
import LinkButton from "./LinkButton.astro";
import { SITE } from "@/config";
const { pathname } = Astro.url;
// Remove trailing slash from current pathname if exists
const currentPath =
pathname.endsWith("/") && pathname !== "/" ? pathname.slice(0, -1) : pathname;
const isActive = (path: string) => {
const currentPathArray = currentPath.split("/").filter(p => p.trim());
const pathArray = path.split("/").filter(p => p.trim());
return currentPath === path || currentPathArray[0] === pathArray[0];
};
---
<header>
<a
id="skip-to-content"
href="#main-content"
class="absolute -top-full left-16 z-50 bg-background px-3 py-2 text-accent backdrop-blur-lg transition-all focus:top-4"
>
Skip to content
</a>
<div
id="nav-container"
class="mx-auto flex max-w-3xl flex-col items-center justify-between sm:flex-row"
>
<div
id="top-nav-wrap"
class="relative flex w-full items-baseline justify-between bg-background p-4 sm:py-6"
>
<a
href="/"
class="absolute py-1 text-2xl leading-7 font-semibold whitespace-nowrap sm:static"
>
{SITE.title}
</a>
<nav
id="nav-menu"
class="flex w-full flex-col items-center sm:ml-2 sm:flex-row sm:justify-end sm:space-x-4 sm:py-0"
>
<button
id="menu-btn"
class="focus-outline self-end p-2 sm:hidden"
aria-label="Open Menu"
aria-expanded="false"
aria-controls="menu-items"
>
<IconX id="close-icon" class="hidden" />
<IconMenuDeep id="menu-icon" />
</button>
<ul
id="menu-items"
class:list={[
"mt-4 grid w-44 grid-cols-2 place-content-center gap-2",
"[&>li>a]:block [&>li>a]:px-4 [&>li>a]:py-3 [&>li>a]:text-center [&>li>a]:font-medium [&>li>a]:hover:text-accent sm:[&>li>a]:px-2 sm:[&>li>a]:py-1",
"hidden",
"sm:mt-0 sm:ml-0 sm:flex sm:w-auto sm:gap-x-5 sm:gap-y-0",
]}
>
<li class="col-span-2">
<a href="/posts" class:list={{ "active-nav": isActive("/posts") }}>
文章
</a>
</li>
<li class="col-span-2">
<a href="/tags" class:list={{ "active-nav": isActive("/tags") }}>
标签
</a>
</li>
<li class="col-span-2">
<a href="/about" class:list={{ "active-nav": isActive("/about") }}>
关于
</a>
</li>
{
SITE.showArchives && (
<li class="col-span-2">
<LinkButton
href="/archives"
class:list={[
"focus-outline flex justify-center p-3 sm:p-1",
{
"active-nav [&>svg]:stroke-accent": isActive("/archives"),
},
]}
ariaLabel="archives"
title="归档"
>
<IconArchive class="hidden sm:inline-block" />
<span class="sm:sr-only">归档</span>
</LinkButton>
</li>
)
}
<li class="col-span-1 flex items-center justify-center">
<LinkButton
href="/search"
class:list={[
"focus-outline flex p-3 sm:p-1",
{ "[&>svg]:stroke-accent": isActive("/search") },
]}
ariaLabel="search"
title="搜索"
>
<IconSearch />
<span class="sr-only">搜索</span>
</LinkButton>
</li>
{
SITE.lightAndDarkMode && (
<li class="col-span-1 flex items-center justify-center">
<button
id="theme-btn"
class="focus-outline relative size-12 p-4 sm:size-8 hover:[&>svg]:stroke-accent"
title="切换明&暗"
aria-label="auto"
aria-live="polite"
>
<IconMoon class="absolute top-[50%] left-[50%] -translate-[50%] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
<IconSunHigh class="absolute top-[50%] left-[50%] -translate-[50%] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
</button>
</li>
)
}
</ul>
</nav>
</div>
</div>
<Hr />
</header>
<script>
function toggleNav() {
const menuBtn = document.querySelector("#menu-btn");
const menuItems = document.querySelector("#menu-items");
const menuIcon = document.querySelector("#menu-icon");
const closeIcon = document.querySelector("#close-icon");
if (!menuBtn || !menuItems || !menuIcon || !closeIcon) return;
menuBtn.addEventListener("click", () => {
const openMenu = menuBtn.getAttribute("aria-expanded") === "true";
menuBtn.setAttribute("aria-expanded", openMenu ? "false" : "true");
menuBtn.setAttribute("aria-label", openMenu ? "Open Menu" : "Close Menu");
menuItems.classList.toggle("hidden");
menuIcon.classList.toggle("hidden");
closeIcon.classList.toggle("hidden");
});
}
toggleNav();
// Runs on view transitions navigation
document.addEventListener("astro:after-swap", toggleNav);
</script>

12
src/components/Hr.astro Normal file
View File

@ -0,0 +1,12 @@
---
export interface Props {
noPadding?: boolean;
ariaHidden?: boolean;
}
const { noPadding = false, ariaHidden = true } = Astro.props;
---
<div class={`max-w-3xl mx-auto ${noPadding ? "px-0" : "px-4"}`}>
<hr class="border-border" aria-hidden={ariaHidden} />
</div>

View File

@ -0,0 +1,42 @@
---
export interface Props {
id?: string;
href: string;
class?: string;
ariaLabel?: string;
title?: string;
disabled?: boolean;
}
const {
id,
href,
class: className = "",
ariaLabel,
title,
disabled = false,
} = Astro.props;
---
{
disabled ? (
<span
id={id}
class:list={["group inline-block", className]}
title={title}
aria-disabled={disabled}
>
<slot />
</span>
) : (
<a
id={id}
{href}
class:list={["group inline-block hover:text-accent", className]}
aria-label={ariaLabel}
title={title}
>
<slot />
</a>
)
}

View File

@ -0,0 +1,39 @@
---
import type { Page } from "astro";
import type { CollectionEntry } from "astro:content";
import IconArrowLeft from "@/assets/icons/IconArrowLeft.svg";
import IconArrowRight from "@/assets/icons/IconArrowRight.svg";
import LinkButton from "./LinkButton.astro";
export interface Props {
page: Page<CollectionEntry<"blog">>;
}
const { page } = Astro.props;
---
{
page.lastPage > 1 && (
<nav class="mt-auto mb-8 flex justify-center" aria-label="Pagination">
<LinkButton
disabled={!page.url.prev}
href={page.url.prev as string}
class:list={["mr-4 select-none", { "opacity-50": !page.url.prev }]}
ariaLabel="Previous"
>
<IconArrowLeft class="inline-block" />
Prev
</LinkButton>
{page.currentPage} / {page.lastPage}
<LinkButton
disabled={!page.url.next}
href={page.url.next as string}
class:list={["ml-4 select-none", { "opacity-50": !page.url.next }]}
ariaLabel="Next"
>
下一页
<IconArrowRight class="inline-block" />
</LinkButton>
</nav>
)
}

View File

@ -0,0 +1,26 @@
---
import { SHARE_LINKS } from "@/constants";
import LinkButton from "./LinkButton.astro";
const URL = Astro.url;
---
<div
class="flex flex-col flex-wrap items-center justify-center gap-1 sm:items-start"
>
<span class="italic">分享此文章到:</span>
<div class="text-center">
{
SHARE_LINKS.map(social => (
<LinkButton
href={`${social.href + URL}`}
class="scale-90 p-2 hover:rotate-6 sm:p-1"
title={social.linkTitle}
>
<social.icon class="inline-block size-6 scale-125 fill-transparent stroke-current stroke-2 opacity-90 group-hover:fill-transparent sm:scale-110" />
<span class="sr-only">{social.linkTitle}</span>
</LinkButton>
))
}
</div>
</div>

View File

@ -0,0 +1,25 @@
---
import { SOCIALS } from "@/constants";
import LinkButton from "./LinkButton.astro";
export interface Props {
centered?: boolean;
}
const { centered = false } = Astro.props;
---
<div class:list={["flex-wrap justify-center gap-1", { flex: centered }]}>
{
SOCIALS.map(social => (
<LinkButton
href={social.href}
class="p-2 hover:rotate-6 sm:p-1"
title={social.linkTitle}
>
<social.icon class="inline-block size-6 scale-125 fill-transparent stroke-current stroke-2 opacity-90 group-hover:fill-transparent sm:scale-110" />
<span class="sr-only">{social.linkTitle}</span>
</LinkButton>
))
}
</div>

29
src/components/Tag.astro Normal file
View File

@ -0,0 +1,29 @@
---
import IconHash from "@/assets/icons/IconHash.svg";
export interface Props {
tag: string;
tagName: string;
size?: "sm" | "lg";
}
const { tag, tagName, size = "sm" } = Astro.props;
---
<li
class:list={[
"group inline-block group-hover:cursor-pointer",
size === "sm" ? "my-1 underline-offset-4" : "mx-1 my-3 underline-offset-8",
]}
>
<a
href={`/tags/${tag}/`}
transition:name={tag}
class:list={[
"relative pr-2 text-lg underline decoration-dashed group-hover:-top-0.5 group-hover:text-accent focus-visible:p-1",
{ "text-sm": size === "sm" },
]}
>
#&nbsp;<span>{tagName}</span>
</a>
</li>

19
src/config.ts Normal file
View File

@ -0,0 +1,19 @@
export const SITE = {
website: "https://cape.blue/", // replace this with your deployed domain
author: "ZephyrG",
profile: "https://赵文光.中国/",
desc: "一个分享技术、文学的博客",
title: "蓝色的海角",
ogImage: "astropaper-og.jpg",
lightAndDarkMode: true,
postPerIndex: 4,
postPerPage: 4,
scheduledPostMargin: 15 * 60 * 1000, // 15 minutes
showArchives: true,
showBackButton: true, // show back button in post detail
editPost: {
url: "https://github.com/blue-cape/astro-paper-zh/edit/main/src/content/blog",
text: "建议修改",
appendFilePath: true,
},
} as const;

80
src/constants.ts Normal file
View File

@ -0,0 +1,80 @@
import IconMail from "@/assets/icons/IconMail.svg";
import IconGitHub from "@/assets/icons/IconGitHub.svg";
import IconBrandX from "@/assets/icons/IconBrandX.svg";
import IconLinkedin from "@/assets/icons/IconLinkedin.svg";
import IconWhatsapp from "@/assets/icons/IconWhatsapp.svg";
import IconFacebook from "@/assets/icons/IconFacebook.svg";
import IconTelegram from "@/assets/icons/IconTelegram.svg";
import IconPinterest from "@/assets/icons/IconPinterest.svg";
import { SITE } from "@/config";
export const LOCALE = {
lang: "zh", // html lang code. Set this empty and default will be "en"
langTag: ["zh-CN"], // BCP 47 Language Tags. Set this empty [] to use the environment default
} as const;
export const SOCIALS = [
{
name: "Github",
href: "https://github.com/blue-cape",
linkTitle: ` ${SITE.title} on Github`,
icon: IconGitHub,
},
{
name: "X",
href: "https://x.com/ZephyrG",
linkTitle: `${SITE.title} on X`,
icon: IconBrandX,
},
{
name: "LinkedIn",
href: "https://www.linkedin.com/in/ZephyrG/",
linkTitle: `${SITE.title} on LinkedIn`,
icon: IconLinkedin,
},
{
name: "Mail",
href: "mailto:blue-cape@outlook.com",
linkTitle: `Send an email to ${SITE.title}`,
icon: IconMail,
},
] as const;
export const SHARE_LINKS = [
{
name: "WhatsApp",
href: "https://wa.me/?text=",
linkTitle: `Share this post via WhatsApp`,
icon: IconWhatsapp,
},
{
name: "Facebook",
href: "https://www.facebook.com/sharer.php?u=",
linkTitle: `Share this post on Facebook`,
icon: IconFacebook,
},
{
name: "X",
href: "https://x.com/intent/post?url=",
linkTitle: `Share this post on X`,
icon: IconBrandX,
},
{
name: "Telegram",
href: "https://t.me/share/url?url=",
linkTitle: `Share this post via Telegram`,
icon: IconTelegram,
},
{
name: "Pinterest",
href: "https://pinterest.com/pin/create/button/?url=",
linkTitle: `Share this post on Pinterest`,
icon: IconPinterest,
},
{
name: "Mail",
href: "mailto:?subject=See%20this%20post&body=",
linkTitle: `Share this post via email`,
icon: IconMail,
},
] as const;

30
src/content.config.ts Normal file
View File

@ -0,0 +1,30 @@
import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";
import { SITE } from "@/config";
const blog = defineCollection({
loader: glob({ pattern: "**/**/[^_]*.md", base: "./src/data/blog" }),
schema: ({ image }) =>
z.object({
author: z.string().default(SITE.author),
pubDatetime: z.date(),
modDatetime: z.date().optional().nullable(),
title: z.string(),
featured: z.boolean().optional(),
draft: z.boolean().optional(),
tags: z.array(z.string()).default(["others"]),
ogImage: image().or(z.string()).optional(),
description: z.string(),
canonicalURL: z.string().optional(),
editPost: z
.object({
disabled: z.boolean().optional(),
url: z.string().optional(),
text: z.string().optional(),
appendFilePath: z.boolean().optional(),
})
.optional(),
}),
});
export const collections = { blog };

View File

@ -0,0 +1,63 @@
---
title: Gin避免静态资源和Api路由冲突的一种方法
pubDatetime: 2025-02-11T14:02:06+8:00
description: Gin避免静态资源和Api路由冲突的一种方法
slug: Gin避免静态资源和Api路由冲突的一种方法
tags:
- Go语言
- Gin
- Web
---
通常情况下,以下代码编译时会报错:
```go
api := engine.Group("/api")
{
api.GET("/hello", func(ctx *gin.Context) {
//省略
})
}
ser.Static("/","/static")
```
因为存在路由冲突,可通过下述方法解决。
<!-- more -->
```go
func routeSplitMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
if strings.HasPrefix(ctx.Request.URL.Path, "/api") {
ctx.Next()
return
}
if _, err := os.Stat(config.GlobalConfig.Static.Dir + ctx.Request.URL.Path); os.IsNotExist(err) {
ctx.Redirect(301, "/404.html")
ctx.Abort()
return
}
ctx.File("./static" + ctx.Request.URL.Path)
ctx.Abort()
}
}
```
```go
engine.Use(routeSplitMiddleware())
//API路由组
api := engine.Group("/api")
{
api.GET("/hello", func(ctx *gin.Context) {
//省略
})
}
// 处理Api 404
engine.NoRoute(func(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{"error": "404"})
})
```
`routeSplitMiddleware`​中间件可以将非`/api`开头的请求返回静态资源如果静态资源不存在则路由重定向到404.html如果请求URL开头为`/api`​,则请求后面的`api`路由相应路由如果不存在则返回404错误消息。
以上Gin静态资源和Api路由冲突的问题得到解决。

View File

@ -0,0 +1,198 @@
---
title: HTTPS代理理论及实战
pubDatetime: 2025-02-12T09:15:06+8:00
description: HTTPS代理理论及实战
slug: https-proxy
tags:
- HTTPS代理
- 中间人攻击
---
## 场景需求
| **场景** | **用途** |
|----------------|-------------------------------------------------------------------------|
| 开发调试 | 模拟 API 响应,测试客户端逻辑(如支付失败、超时场景) |
| 性能优化 | 缓存静态资源,减少重复请求网络延迟 |
| 安全测试 | 拦截敏感数据传输,检测明文泄露或弱加密问题 |
| 故障排查 | 模拟服务器 5xx 错误,验证客户端容错逻辑 |
| 隐私保护 | 通过代理隐藏真实 IP防止地理位置追踪 |
<!-- more -->
---
## HTTPS 抓包原理
### 1. HTTPS 加密的基本原理
```plaintext
HTTPS 握手流程:
1. 客户端发送 ClientHello → 协商 TLS 版本和加密套件
2. 服务端返回 ServerHello + 证书 → 身份验证
3. 客户端验证证书 → 生成预主密钥Premaster Secret
4. 服务端解密预主密钥 → 生成对称加密密钥Session Key
5. 双方使用 Session Key 加密通信
```
**关键点**
非对称加密仅用于身份验证和密钥交换,对称加密用于实际数据传输。
---
### 2. HTTPS 抓包的核心中间人攻击MITM
```mermaid
sequenceDiagram
participant Client
participant MITM_Proxy
participant Server
Client->>MITM_Proxy: 请求 HTTPS 连接ClientHello
MITM_Proxy->>Client: 返回伪造证书
Client->>MITM_Proxy: 信任证书并协商密钥
MITM_Proxy->>Server: 转发请求并建立真实 HTTPS 连接
Server->>MITM_Proxy: 返回真实加密数据
MITM_Proxy->>Client: 解密后重新加密返回
```
**必要条件**:客户端必须信任代理工具的根证书(否则浏览器会提示 `NET::ERR_CERT_AUTHORITY_INVALID`)。
---
### 3. 抓包工具的具体实现
| **工具** | **特点** |
|---------------|-------------------------------------------------------------------------|
| Fiddler | 图形化界面支持断点调试和自动化脚本FiddlerScript |
| Charles | 类似 Fiddler支持 Map Remote/Local 和 Throttling 功能 |
| mitmproxy | 命令行工具,适合自动化场景,支持 Python 脚本扩展 |
| Wireshark | 底层抓包,需配合 SSLKEYLOGFILE 解密流量 |
---
### 4. 突破 HTTPS 抓包的限制
#### 场景 1证书绑定Certificate Pinning
- **原理**:应用内置服务器证书指纹,拒绝信任系统证书。
- **绕过方案**
```bash
# Android 使用 Frida 绕过(需 root
frida -U -f com.example.app -l bypass_ssl_pinning.js
```
#### 场景 2HTTP/2 或 HTTP/3
- **对策**:确保代理工具支持最新协议(如 mitmproxy 7.0+ 支持 HTTP/2
#### 场景 3客户端检测代理
- **透明代理方案**
```bash
# Linux 使用 iptables 重定向流量
iptables -t nat -A OUTPUT -p tcp --dport 443 -j REDIRECT --to-port 8080
```
---
## 实战mitmproxy 使用指南
### 1. 安装与启动
```bash
# 安装(推荐 Python 虚拟环境)
pip install mitmproxy
# 启动交互式界面
mitmproxy -p 8080 --ssl-insecure
# 启动 Web 界面http://localhost:8081
mitmweb
```
---
### 2. 代理配置
| **客户端类型** | **配置方法** |
|---------------|-----------------------------------------------------------------------------|
| 浏览器 | 设置 → 网络设置 → 手动代理:`127.0.0.1:8080` |
| Android | Wi-Fi → 修改网络 → 高级选项 → 代理手动 → 输入主机和端口 |
| iOS | Wi-Fi → HTTP 代理 → 配置代理 → 手动 |
| 命令行工具 | 使用环境变量:<br>`export http_proxy=http://127.0.0.1:8080 https_proxy=http://127.0.0.1:8080` |
---
### 3. 证书安装
| **系统** | **操作步骤** |
|--------------|-----------------------------------------------------------------------------|
| Windows | 访问 `http://mitm.it` → 下载证书 → 双击安装到“受信任的根证书颁发机构” |
| macOS | 下载证书 → 钥匙串访问 → 添加证书 → 设置为“始终信任” |
| Android | 访问 `http://mitm.it` → 下载证书 → 设置 → 安全 → 加密与凭据 → 安装证书 |
| iOS | 访问 `http://mitm.it` → 下载描述文件 → 设置 → 已下载描述文件 → 安装 |
---
### 4. 基础操作
| **快捷键** | **功能** |
|-------------|-------------------------------------------------------------------------|
| `↑/↓` | 选择请求 |
| `Enter` | 查看请求/响应详情 |
| `Tab` | 切换请求/响应标签页 |
| `/` | 过滤请求(如 `~u example.com` 过滤 URL |
| `w` | 保存会话到文件(`.mitm` 格式) |
---
### 5. 高级功能
#### 脚本示例:修改请求头
```python
# modify_headers.py
from mitmproxy import http
def request(flow: http.HTTPFlow):
flow.request.headers["X-Debug"] = "true"
```
#### 脚本示例:模拟 API 响应
```python
# mock_api.py
from mitmproxy import http
def request(flow: http.HTTPFlow):
if "api.example.com/data" in flow.request.url:
flow.response = http.Response.make(
200,
b'{"status": "success", "data": "mocked"}',
{"Content-Type": "application/json"}
)
```
**启动命令**
```bash
mitmproxy -s modify_headers.py -s mock_api.py
```
---
### 6. 调试技巧
- **实时日志**:通过 `mitmdump -v` 查看详细日志。
- **过滤噪声**:使用 `-I` 参数忽略指定域名:
```bash
mitmproxy -I ".*google.*"
```
- **性能分析**:按 `m` 标记请求,按 `M` 查看统计信息(如响应时间分布)。
---
## 附录
### 工具对比
| **功能** | mitmproxy | Fiddler | Charles |
|------------------|-----------|---------|---------|
| 跨平台支持 | ✔️ | ❌ (Windows) | ✔️ |
| 脚本扩展 | ✔️ (Python) | ✔️ (C#) | ❌ |
| 图形化界面 | ❌ (mitmweb 基础) | ✔️ | ✔️ |
| 协议支持 | HTTP/1.1, HTTP/2 | HTTP/1.1 | HTTP/1.1, HTTP/2 |
### 常见问题
**Q1手机无法安装证书**
- 确保已关闭“证书透明性”监控iOS 设置 → Safari → 高级)。
- Android 7+ 需将证书安装到系统级(需 root
**Q2抓包时出现 `TLS handshake failed`**
- 使用 `--ssl-insecure` 参数忽略证书错误。
- 检查客户端是否信任 mitmproxy 的根证书。

View File

@ -0,0 +1,86 @@
---
title: 绕过disable-devtool限制的方法
pubDatetime: 2025-02-12T17:53:33+08:00
description: 在一些网页开发或调试过程中,可能会遇到网页通过 disable-devtool 来限制开发者工具的使用。以下是两种可以实现绕过 disable-devtool 的方法,但请注意,这些方法仅用于技术探讨和合法的开发调试场景,切勿用于非法用途。
slug: 绕过disable-devtool限制的方法
tags:
- 前端
- 破解
---
在一些网页开发或调试过程中,可能会遇到网页通过 `disable-devtool` 来限制开发者工具的使用。以下是两种可以实现绕过 `disable-devtool` 的方法,但请注意,这些方法仅用于技术探讨和合法的开发调试场景,**切勿用于非法用途**。
## 一、通过 DevTool 的本地替代
1. **准备工作**
提前打开浏览器的开发者工具DevTool进入“网络”Network页面勾选“保留日志”Preserve log选项。这样即使页面发生跳转或关闭依然可以查看到相关的网络请求日志。同时勾选“禁用缓存”Disable cache选项防止浏览器直接使用未修改的缓存文件。
2. **寻找目标文件**
打开需要调试的网页,仔细查看网络请求列表,找到一个包含 `tkName``disable-devtool` 等关键字的 `.js` 文件。这通常是网页用来检测开发者工具的关键脚本。
3. **调整网络速度**
在网络页面中将网络条件从“无限制”No throttling调整为“慢速 4G”Slow 4G或“3G”3G。这样可以减缓页面跳转或关闭的速度为我们后续的操作争取时间。
4. **修改文件**
在“源代码”Sources页面中找到刚才定位到的 `.js` 文件点击暂停调试按钮Pause然后在“覆盖”Overrides选项中选择一个替代文件夹。接着右键点击要修改的 `.js` 文件选择“替代”Override
找到文件中类似以下的代码片段:
```javascript
(d.tkName)) === d.md5) return t("token passed");
```
将其修改为:
```javascript
(d.tkName)) === d.md5, true) return t("token passed");
```
5. **完成绕过**
刷新页面,此时 `disable-devtool` 的限制已被成功绕过。
## 二、通过 Mitmproxy 等 HTTPS 代理的中间人模式替代远程响应
1. **基础操作**
与第一种方法类似,禁用缓存、先找到目标 `.js` 文件,不再赘述。
2. **本地文件准备**
将找到的 `.js` 文件另存到本地,并按照第一种方法中的步骤修改该文件。
3. **配置 Mitmproxy 插件**
使用以下 Mitmproxy 插件代码,实现对远程响应的替换,从而绕过 `disable-devtool`。具体使用方法可参考相关教程。
```python
from mitmproxy import http
from mitmproxy.http import Response
def request(flow: http.HTTPFlow):
if "hello" in flow.request.url: # 替换为需要替代的远程 URL 条件
try:
with open("本地文件路径", encoding='utf-8') as f: # 替换为本地文件路径
flow.response = Response.make(
200,
f.read(),
{"Content-Type": "application/javascript"}
)
except UnicodeDecodeError:
# 如果 UTF-8 编码失败,尝试以二进制方式读取
with open("本地文件路径", 'rb') as f: # 替换为本地文件路径
flow.response = Response.make(
200,
f.read(),
{"Content-Type": "application/javascript"}
)
```
4. **实现绕过**
启动 Mitmproxy 并加载该插件当访问目标网页时Mitmproxy 会自动将修改后的本地 `.js` 文件替换远程响应的 `.js` 文件,从而绕过 `disable-devtool`
### 方法对比
* **第一种方法**:操作相对简单,适合手动调试场景。但如果使用 Selenium 等自动化调试工具,这种方法可能会失效。
* **第二种方法**:虽然操作相对复杂,但可以兼容 Selenium 等自动化调试工具,适合需要自动化操作的场景。
* **第三种方法**:此外,如果网站仅使用 CDN 的 `disable-devtool.js` 文件,还可以通过在安全软件中禁止请求该 URL 来实现绕过。
## 三、重要声明
**再次强调**,以上方法仅供技术探讨和合法的开发调试使用,**切勿用于非法用途**。任何未经授权的绕过行为都可能违反法律法规,导致严重的法律后果。请始终确保您的行为符合法律和道德规范。

View File

@ -0,0 +1,47 @@
---
title: 《七律·和郭沫若同志》——毛泽东
pubDatetime: 2025-03-08T20:00:00+08:00
description: 毛泽东通过神话重构,将革命话语融入传统叙事,展现独特的政治诗学风格。
slug: 七律-和郭沫若同志
tags:
- 诗歌
---
## 原文
**《七律·和郭沫若同志》**
一从大地起风雷,便有精生白骨堆。
僧是愚氓犹可训,妖为鬼蜮必成灾。
金猴奋起千钧棒,玉宇澄清万里埃。
今日欢呼孙大圣,只缘妖雾又重来。
## 赏析
### 1. 历史背景与政治隐喻
此诗作于1961年是对郭沫若《看〈孙悟空三打白骨精〉》一诗的和作。时值国际共产主义运动分歧加剧中苏论战国内处于经济困难时期。毛泽东借《西游记》中孙悟空三打白骨精的典故以寓言形式表达对阶级斗争的深刻思考。
**意象解析:**
**"白骨堆"**:象征腐朽落后的反动势力,暗指国际修正主义及国内敌对力量。
**"僧"(唐僧)**:指代思想动摇但可争取的中间派,体现"团结—批评—团结"策略。
**"妖"(白骨精)**:喻指不可调和的阶级敌人,强调其破坏性本质。
**"金猴"(孙悟空)**:无产阶级革命力量的化身,"千钧棒"象征革命武装斗争。
---
### 2. 艺术特色
**革命浪漫主义**:将神话意象与政治现实结合,如"玉宇澄清"展现理想社会图景。
**对比手法**:通过"僧"与"妖"的对比,强化敌我矛盾的不可调和性。
**用典创新**:颠覆传统对唐僧的同情式解读,体现阶级分析视角。
---
### 3. 哲学内涵
尾联"妖雾又重来"揭示斗争长期性,呼应"阶级斗争要年年讲,月月讲"的政治理念。诗中体现的"矛盾斗争推动历史发展"思想,是其辩证法哲学的诗意表达。
## 思想价值
该诗以文艺形式阐述政治主张,体现毛泽东"古为今用"的文艺观。其核心思想——在复杂斗争中保持革命警惕性,对当代仍具启示意义。

View File

@ -0,0 +1,28 @@
---
import Header from "@/components/Header.astro";
import Footer from "@/components/Footer.astro";
import Breadcrumb from "@/components/Breadcrumb.astro";
import Layout from "./Layout.astro";
import { SITE } from "@/config";
export interface Props {
frontmatter: {
title: string;
description?: string;
};
}
const { frontmatter } = Astro.props;
---
<Layout title={`${frontmatter.title} | ${SITE.title}`}>
<Header />
<Breadcrumb />
<main id="main-content">
<section id="about" class="prose mb-28 max-w-3xl prose-img:border-0">
<h1 class="text-2xl tracking-wider sm:text-3xl">{frontmatter.title}</h1>
<slot />
</section>
</main>
<Footer />
</Layout>

148
src/layouts/Layout.astro Normal file
View File

@ -0,0 +1,148 @@
---
import { ClientRouter } from "astro:transitions";
import { SITE } from "@/config";
import { LOCALE } from "@/constants";
import "@/styles/global.css";
const googleSiteVerification = import.meta.env.PUBLIC_GOOGLE_SITE_VERIFICATION;
export interface Props {
title?: string;
author?: string;
profile?: string;
description?: string;
ogImage?: string;
canonicalURL?: string;
pubDatetime?: Date;
modDatetime?: Date | null;
scrollSmooth?: boolean;
}
const {
title = SITE.title,
author = SITE.author,
profile = SITE.profile,
description = SITE.desc,
ogImage = SITE.ogImage,
canonicalURL = new URL(Astro.url.pathname, Astro.url),
pubDatetime,
modDatetime,
scrollSmooth = false,
} = Astro.props;
const socialImageURL = new URL(ogImage ?? SITE.ogImage ?? "og.png", Astro.url);
const structuredData = {
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: `${title}`,
image: `${socialImageURL}`,
datePublished: `${pubDatetime?.toISOString()}`,
...(modDatetime && { dateModified: modDatetime.toISOString() }),
author: [
{
"@type": "Person",
name: `${author}`,
...(profile && { url: profile }),
},
],
};
---
<!doctype html>
<html
lang=`${LOCALE.lang ?? "en"}`
class={`${scrollSmooth && "scroll-smooth"}`}
>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/avatar.jpg" />
<link rel="canonical" href={canonicalURL} />
<meta name="generator" content={Astro.generator} />
<!-- General Meta Tags -->
<title>{title}</title>
<meta name="title" content={title} />
<meta name="description" content={description} />
<meta name="author" content={author} />
<link rel="sitemap" href="/sitemap-index.xml" />
<!-- Open Graph / Facebook -->
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonicalURL} />
<meta property="og:image" content={socialImageURL} />
<!-- Article Published/Modified time -->
{
pubDatetime && (
<meta
property="article:published_time"
content={pubDatetime.toISOString()}
/>
)
}
{
modDatetime && (
<meta
property="article:modified_time"
content={modDatetime.toISOString()}
/>
)
}
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={canonicalURL} />
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={socialImageURL} />
<!-- Google JSON-LD Structured data -->
<script
type="application/ld+json"
is:inline
set:html={JSON.stringify(structuredData)}
/>
<!-- Enable RSS feed auto-discovery -->
<!-- https://docs.astro.build/en/recipes/rss/#enabling-rss-feed-auto-discovery -->
<link
rel="alternate"
type="application/rss+xml"
title={SITE.title}
href={new URL("rss.xml", Astro.site)}
/>
<meta name="theme-color" content="" />
{
// If PUBLIC_GOOGLE_SITE_VERIFICATION is set in the environment variable,
// include google-site-verification tag in the heading
// Learn more: https://support.google.com/webmasters/answer/9008080#meta_tag_verification&zippy=%2Chtml-tag
googleSiteVerification && (
<meta
name="google-site-verification"
content={googleSiteVerification}
/>
)
}
<ClientRouter />
<script is:inline src="/toggle-theme.js" async></script>
</head>
<body>
<slot />
</body>
</html>
<style>
html,
body {
margin: 0;
width: 100%;
height: 100%;
}
</style>

54
src/layouts/Main.astro Normal file
View File

@ -0,0 +1,54 @@
---
import Breadcrumb from "@/components/Breadcrumb.astro";
import { SITE } from "@/config";
interface StringTitleProp {
pageTitle: string;
pageDesc?: string;
}
interface ArrayTitleProp {
pageTitle: [string, string];
titleTransition: string;
pageDesc?: string;
}
export type Props = StringTitleProp | ArrayTitleProp;
const { props } = Astro;
const backUrl = SITE.showBackButton ? Astro.url.pathname : "/";
---
<Breadcrumb />
<main
data-backUrl={backUrl}
id="main-content"
class="mx-auto w-full max-w-3xl px-4 pb-4"
>
{
"titleTransition" in props ? (
<h1 class="text-2xl font-semibold sm:text-3xl">
{props.pageTitle[0]}
<span transition:name={props.titleTransition}>
{props.pageTitle[1]}
</span>
</h1>
) : (
<h1 class="text-2xl font-semibold sm:text-3xl">{props.pageTitle}</h1>
)
}
<p class="mt-2 mb-6 italic">{props.pageDesc}</p>
<slot />
</main>
<script>
document.addEventListener("astro:page-load", () => {
const mainContent: HTMLElement | null =
document.querySelector("#main-content");
const backUrl = mainContent?.dataset?.backurl;
if (backUrl) {
sessionStorage.setItem("backUrl", backUrl);
}
});
</script>

View File

@ -0,0 +1,277 @@
---
import { render, type CollectionEntry } from "astro:content";
import Layout from "@/layouts/Layout.astro";
import Header from "@/components/Header.astro";
import Footer from "@/components/Footer.astro";
import Tag from "@/components/Tag.astro";
import Datetime from "@/components/Datetime.astro";
import EditPost from "@/components/EditPost.astro";
import ShareLinks from "@/components/ShareLinks.astro";
import BackButton from "@/components/BackButton.astro";
import { slugifyStr } from "@/utils/slugify";
import IconChevronLeft from "@/assets/icons/IconChevronLeft.svg";
import IconChevronRight from "@/assets/icons/IconChevronRight.svg";
import { SITE } from "@/config";
export interface Props {
post: CollectionEntry<"blog">;
posts: CollectionEntry<"blog">[];
}
const { post, posts } = Astro.props;
const {
title,
author,
description,
ogImage,
canonicalURL,
pubDatetime,
modDatetime,
tags,
editPost,
} = post.data;
const { Content } = await render(post);
const ogImageUrl = typeof ogImage === "string" ? ogImage : ogImage?.src;
const ogUrl = new URL(
ogImageUrl ?? `/posts/${slugifyStr(title)}/index.png`,
Astro.url.origin
).href;
const layoutProps = {
title: `${title} | ${SITE.title}`,
author,
description,
pubDatetime,
modDatetime,
canonicalURL,
ogImage: ogUrl,
scrollSmooth: true,
};
/* ========== Prev/Next Posts ========== */
const allPosts = posts.map(({ data: { title }, id }) => ({
slug: id,
title,
}));
const currentPostIndex = allPosts.findIndex(a => a.slug === post.id);
const prevPost = currentPostIndex !== 0 ? allPosts[currentPostIndex - 1] : null;
const nextPost =
currentPostIndex !== allPosts.length ? allPosts[currentPostIndex + 1] : null;
---
<Layout {...layoutProps}>
<Header />
<BackButton />
<main
id="main-content"
class:list={[
"mx-auto w-full max-w-3xl px-4 pb-12",
{ "mt-8": !SITE.showBackButton },
]}
data-pagefind-body
>
<h1
transition:name={slugifyStr(title)}
class="inline-block text-2xl font-bold text-accent sm:text-3xl"
>
{title}
</h1>
<div class="flex items-center gap-4">
<Datetime
pubDatetime={pubDatetime}
modDatetime={modDatetime}
size="lg"
class="my-2"
/>
<EditPost class="max-sm:hidden" editPost={editPost} postId={post.id} />
</div>
<article id="article" class="mx-auto prose mt-8 max-w-3xl">
<Content />
</article>
<hr class="my-8 border-dashed" />
<EditPost class="sm:hidden" editPost={editPost} postId={post.id} />
<ul class="mt-4 mb-8 sm:my-8">
{tags.map(tag => <Tag tag={slugifyStr(tag)} tagName={tag} />)}
</ul>
<div
class="flex flex-col items-center justify-between gap-6 sm:flex-row sm:items-end sm:gap-4"
>
<ShareLinks />
<button
id="back-to-top"
class="focus-outline py-1 whitespace-nowrap hover:opacity-75"
>
<IconChevronLeft class="inline-block rotate-90" />
<span>返回顶部</span>
</button>
</div>
<hr class="my-6 border-dashed" />
<!-- Previous/Next Post Buttons -->
<div data-pagefind-ignore class="grid grid-cols-1 gap-6 sm:grid-cols-2">
{
prevPost && (
<a
href={`/posts/${prevPost.slug}`}
class="flex w-full gap-1 hover:opacity-75"
>
<IconChevronLeft class="inline-block flex-none" />
<div>
<span>上一篇文章</span>
<div class="text-sm text-accent/85">{prevPost.title}</div>
</div>
</a>
)
}
{
nextPost && (
<a
href={`/posts/${nextPost.slug}`}
class="flex w-full justify-end gap-1 text-right hover:opacity-75 sm:col-start-2"
>
<div>
<span>下一篇文章</span>
<div class="text-sm text-accent/85">{nextPost.title}</div>
</div>
<IconChevronRight class="inline-block flex-none" />
</a>
)
}
</div>
</main>
<Footer />
</Layout>
<script is:inline data-astro-rerun>
/** Create a progress indicator
* at the top */
function createProgressBar() {
// Create the main container div
const progressContainer = document.createElement("div");
progressContainer.className =
"progress-container fixed top-0 z-10 h-1 w-full bg-background";
// Create the progress bar div
const progressBar = document.createElement("div");
progressBar.className = "progress-bar h-1 w-0 bg-accent";
progressBar.id = "myBar";
// Append the progress bar to the progress container
progressContainer.appendChild(progressBar);
// Append the progress container to the document body or any other desired parent element
document.body.appendChild(progressContainer);
}
createProgressBar();
/** Update the progress bar
* when user scrolls */
function updateScrollProgress() {
document.addEventListener("scroll", () => {
const winScroll =
document.body.scrollTop || document.documentElement.scrollTop;
const height =
document.documentElement.scrollHeight -
document.documentElement.clientHeight;
const scrolled = (winScroll / height) * 100;
if (document) {
const myBar = document.getElementById("myBar");
if (myBar) {
myBar.style.width = scrolled + "%";
}
}
});
}
updateScrollProgress();
/** Attaches links to headings in the document,
* allowing sharing of sections easily */
function addHeadingLinks() {
const headings = Array.from(
document.querySelectorAll("h2, h3, h4, h5, h6")
);
for (const heading of headings) {
heading.classList.add("group");
const link = document.createElement("a");
link.className =
"heading-link ml-2 opacity-0 group-hover:opacity-100 focus:opacity-100";
link.href = "#" + heading.id;
const span = document.createElement("span");
span.ariaHidden = "true";
span.innerText = "#";
link.appendChild(span);
heading.appendChild(link);
}
}
addHeadingLinks();
/** Attaches copy buttons to code blocks in the document,
* allowing users to copy code easily. */
function attachCopyButtons() {
const copyButtonLabel = "Copy";
const codeBlocks = Array.from(document.querySelectorAll("pre"));
for (const codeBlock of codeBlocks) {
const wrapper = document.createElement("div");
wrapper.style.position = "relative";
const copyButton = document.createElement("button");
copyButton.className =
"copy-code absolute right-3 -top-3 rounded bg-muted px-2 py-1 text-xs leading-4 text-foreground font-medium";
copyButton.innerHTML = copyButtonLabel;
codeBlock.setAttribute("tabindex", "0");
codeBlock.appendChild(copyButton);
// wrap codebock with relative parent element
codeBlock?.parentNode?.insertBefore(wrapper, codeBlock);
wrapper.appendChild(codeBlock);
copyButton.addEventListener("click", async () => {
await copyCode(codeBlock, copyButton);
});
}
async function copyCode(block, button) {
const code = block.querySelector("code");
const text = code?.innerText;
await navigator.clipboard.writeText(text ?? "");
// visual feedback that task is completed
button.innerText = "Copied";
setTimeout(() => {
button.innerText = copyButtonLabel;
}, 700);
}
}
attachCopyButtons();
/** Scrolls the document to the top when
* the "Back to Top" button is clicked. */
function backToTop() {
document.querySelector("#back-to-top")?.addEventListener("click", () => {
document.body.scrollTop = 0; // For Safari
document.documentElement.scrollTop = 0; // For Chrome, Firefox, IE and Opera
});
}
backToTop();
/* Go to page start after page swap */
document.addEventListener("astro:after-swap", () =>
window.scrollTo({ left: 0, top: 0, behavior: "instant" })
);
</script>

30
src/pages/404.astro Normal file
View File

@ -0,0 +1,30 @@
---
import Layout from "@/layouts/Layout.astro";
import Header from "@/components/Header.astro";
import Footer from "@/components/Footer.astro";
import LinkButton from "@/components/LinkButton.astro";
import { SITE } from "@/config";
---
<Layout title={`404 Not Found | ${SITE.title}`}>
<Header />
<main
id="main-content"
class="mx-auto flex max-w-3xl flex-1 items-center justify-center"
>
<div class="mb-14 flex flex-col items-center justify-center">
<h1 class="text-9xl font-bold text-accent">404</h1>
<span aria-hidden="true">¯\_(ツ)_/¯</span>
<p class="mt-4 text-2xl sm:text-3xl">Page Not Found</p>
<LinkButton
href="/"
class="my-6 text-lg underline decoration-dashed underline-offset-8"
>
Go back home
</LinkButton>
</div>
</main>
<Footer />
</Layout>

31
src/pages/about.md Normal file
View File

@ -0,0 +1,31 @@
---
layout: ../layouts/AboutLayout.astro
title: "关于"
---
本站的名字起源于普希金的《囚徒》一诗:
> 我坐在阴湿牢狱的铁栏后。
>
> 一只在禁锢中成长的鹰雏
>
> 和我郁郁地做伴;它扑着翅膀,
>
> 在铁窗下啄食着血腥的食物。
>
> 它啄食着,丢弃着,又望望窗外,
>
> 像是和我感到同样的烦恼。
>
> 它用眼神和叫声向我招呼,
>
> 像要说:“我们飞去吧,是时候了,
>
> “我们原是自由的鸟儿,飞去吧——
>
> 飞到那乌云后面明媚的山峦,
>
> 飞到那里,到那蓝色的海角,
>
> 只有风在欢舞……还有我做伴!……”
愿永远自由!

View File

@ -0,0 +1,83 @@
---
import { getCollection } from "astro:content";
import Main from "@/layouts/Main.astro";
import Layout from "@/layouts/Layout.astro";
import Header from "@/components/Header.astro";
import Footer from "@/components/Footer.astro";
import Card from "@/components/Card.astro";
import getPostsByGroupCondition from "@/utils/getPostsByGroupCondition";
import { SITE } from "@/config";
// Redirect to 404 page if `showArchives` config is false
if (!SITE.showArchives) {
return Astro.redirect("/404");
}
const posts = await getCollection("blog", ({ data }) => !data.draft);
const months = [
"一月",
"二月",
"三月",
"四月",
"五月",
"六月",
"七月",
"八月",
"九月",
"十月",
"十一月",
"十二月",
];
---
<Layout title={`归档 | ${SITE.title}`}>
<Header />
<Main pageTitle="归档" pageDesc="归档的所有文章。">
{
Object.entries(
getPostsByGroupCondition(posts, post =>
post.data.pubDatetime.getFullYear()
)
)
.sort(([yearA], [yearB]) => Number(yearB) - Number(yearA))
.map(([year, yearGroup]) => (
<div>
<span class="text-2xl font-bold">{year}</span>
<sup class="text-sm">{yearGroup.length}</sup>
{Object.entries(
getPostsByGroupCondition(
yearGroup,
post => post.data.pubDatetime.getMonth() + 1
)
)
.sort(([monthA], [monthB]) => Number(monthB) - Number(monthA))
.map(([month, monthGroup]) => (
<div class="flex flex-col sm:flex-row">
<div class="mt-6 min-w-36 text-lg sm:my-6">
<span class="font-bold">{months[Number(month) - 1]}</span>
<sup class="text-xs">{monthGroup.length}</sup>
</div>
<ul>
{monthGroup
.sort(
(a, b) =>
Math.floor(
new Date(b.data.pubDatetime).getTime() / 1000
) -
Math.floor(
new Date(a.data.pubDatetime).getTime() / 1000
)
)
.map(({ data, id }) => (
<Card href={`/posts/${id}`} frontmatter={data} />
))}
</ul>
</div>
))}
</div>
))
}
</Main>
<Footer />
</Layout>

128
src/pages/index.astro Normal file
View File

@ -0,0 +1,128 @@
---
import { getCollection } from "astro:content";
import Layout from "@/layouts/Layout.astro";
import Header from "@/components/Header.astro";
import Footer from "@/components/Footer.astro";
import Socials from "@/components/Socials.astro";
import LinkButton from "@/components/LinkButton.astro";
import Card from "@/components/Card.astro";
import Hr from "@/components/Hr.astro";
import getSortedPosts from "@/utils/getSortedPosts";
import IconRss from "@/assets/icons/IconRss.svg";
import IconArrowRight from "@/assets/icons/IconArrowRight.svg";
import { SITE } from "@/config";
import { SOCIALS } from "@/constants";
const posts = await getCollection("blog");
const sortedPosts = getSortedPosts(posts);
const featuredPosts = sortedPosts.filter(({ data }) => data.featured);
const recentPosts = sortedPosts.filter(({ data }) => !data.featured);
---
<Layout>
<Header />
<main id="main-content" data-layout="index">
<section id="hero" class="pt-8 pb-6">
<h1 class="my-4 inline-block text-4xl font-bold sm:my-8 sm:text-5xl">
Zephyr
</h1>
<a
target="_blank"
href="/rss.xml"
class="inline-block"
aria-label="rss feed"
title="RSS Feed"
>
<IconRss
width={20}
height={20}
class="scale-125 stroke-accent stroke-3"
/>
<span class="sr-only">RSS Feed</span>
</a>
<p>
有人生来就被幸福拥抱,有人生来就被长夜围绕。
</p>
<!-- <p class="mt-2">
Read the blog posts or check
<LinkButton
class="underline decoration-dashed underline-offset-4 hover:text-accent"
href="https://github.com/satnaing/astro-paper#readme"
>
README
</LinkButton> for more info.
</p> -->
{
// only display if at least one social link is enabled
SOCIALS.length > 0 && (
<div class="mt-4 flex flex-col sm:flex-row sm:items-center">
<div class="mr-2 mb-1 whitespace-nowrap sm:mb-0">社交链接:</div>
<Socials />
</div>
)
}
</section>
<Hr />
{
featuredPosts.length > 0 && (
<>
<section id="featured" class="pt-12 pb-6">
<h2 class="text-2xl font-semibold tracking-wide">Featured</h2>
<ul>
{featuredPosts.map(({ data, id }) => (
<Card
href={`/posts/${id}/`}
frontmatter={data}
secHeading={false}
/>
))}
</ul>
</section>
{recentPosts.length > 0 && <Hr />}
</>
)
}
{
recentPosts.length > 0 && (
<section id="recent-posts" class="pt-12 pb-6">
<h2 class="text-2xl font-semibold tracking-wide">最新文章</h2>
<ul>
{recentPosts.map(
({ data, id }, index) =>
index < SITE.postPerIndex && (
<Card
href={`/posts/${id}/`}
frontmatter={data}
secHeading={false}
/>
)
)}
</ul>
</section>
)
}
<div class="my-8 text-center">
<LinkButton href="/posts/">
所有文章
<IconArrowRight class="inline-block" />
</LinkButton>
</div>
</main>
<Footer />
</Layout>
<script>
document.addEventListener("astro:page-load", () => {
const indexLayout = (document.querySelector("#main-content") as HTMLElement)
?.dataset?.layout;
if (indexLayout) {
sessionStorage.setItem("backUrl", "/");
}
});
</script>

7
src/pages/og.png.ts Normal file
View File

@ -0,0 +1,7 @@
import type { APIRoute } from "astro";
import { generateOgImageForSite } from "@/utils/generateOgImages";
export const GET: APIRoute = async () =>
new Response(await generateOgImageForSite(), {
headers: { "Content-Type": "image/png" },
});

View File

@ -0,0 +1,36 @@
---
import type { GetStaticPaths } from "astro";
import { getCollection } from "astro:content";
import Main from "@/layouts/Main.astro";
import Layout from "@/layouts/Layout.astro";
import Header from "@/components/Header.astro";
import Footer from "@/components/Footer.astro";
import Card from "@/components/Card.astro";
import Pagination from "@/components/Pagination.astro";
import getSortedPosts from "@/utils/getSortedPosts";
import { SITE } from "@/config";
export const getStaticPaths = (async ({ paginate }) => {
const posts = await getCollection("blog", ({ data }) => !data.draft);
return paginate(getSortedPosts(posts), { pageSize: SITE.postPerPage });
}) satisfies GetStaticPaths;
const { page } = Astro.props;
---
<Layout title={`文章 | ${SITE.title}`}>
<Header />
<Main pageTitle="文章" pageDesc="所有文章">
<ul>
{
page.data.map(({ data, id }) => (
<Card href={`/posts/${id}`} frontmatter={data} />
))
}
</ul>
</Main>
<Pagination {page} />
<Footer noMarginTop={page.lastPage > 1} />
</Layout>

View File

@ -0,0 +1,27 @@
---
import { type CollectionEntry, getCollection } from "astro:content";
import PostDetails from "@/layouts/PostDetails.astro";
import getSortedPosts from "@/utils/getSortedPosts";
export interface Props {
post: CollectionEntry<"blog">;
}
export async function getStaticPaths() {
const posts = await getCollection("blog", ({ data }) => !data.draft);
const postResult = posts.map(post => ({
params: { slug: post.id },
props: { post },
}));
return postResult;
}
const { post } = Astro.props;
const posts = await getCollection("blog");
const sortedPosts = getSortedPosts(posts);
---
<PostDetails post={post} posts={sortedPosts} />

View File

@ -0,0 +1,27 @@
---
import { type CollectionEntry, getCollection } from "astro:content";
import PostDetails from "@/layouts/PostDetails.astro";
import getSortedPosts from "@/utils/getSortedPosts";
export interface Props {
post: CollectionEntry<"blog">;
}
export async function getStaticPaths() {
const posts = await getCollection("blog", ({ data }) => !data.draft);
const postResult = posts.map(post => ({
params: { slug: post.id },
props: { post },
}));
return postResult;
}
const { post } = Astro.props;
const posts = await getCollection("blog");
const sortedPosts = getSortedPosts(posts);
---
<PostDetails post={post} posts={sortedPosts} />

View File

@ -0,0 +1,20 @@
import type { APIRoute } from "astro";
import { getCollection, type CollectionEntry } from "astro:content";
import { generateOgImageForPost } from "@/utils/generateOgImages";
import { slugifyStr } from "@/utils/slugify";
export async function getStaticPaths() {
const posts = await getCollection("blog").then(p =>
p.filter(({ data }) => !data.draft && !data.ogImage)
);
return posts.map(post => ({
params: { slug: slugifyStr(post.data.title) },
props: post,
}));
}
export const GET: APIRoute = async ({ props }) =>
new Response(await generateOgImageForPost(props as CollectionEntry<"blog">), {
headers: { "Content-Type": "image/png" },
});

13
src/pages/robots.txt.ts Normal file
View File

@ -0,0 +1,13 @@
import type { APIRoute } from "astro";
const getRobotsTxt = (sitemapURL: URL) => `
User-agent: *
Allow: /
Sitemap: ${sitemapURL.href}
`;
export const GET: APIRoute = ({ site }) => {
const sitemapURL = new URL("sitemap-index.xml", site);
return new Response(getRobotsTxt(sitemapURL));
};

20
src/pages/rss.xml.ts Normal file
View File

@ -0,0 +1,20 @@
import rss from "@astrojs/rss";
import { getCollection } from "astro:content";
import getSortedPosts from "@/utils/getSortedPosts";
import { SITE } from "@/config";
export async function GET() {
const posts = await getCollection("blog");
const sortedPosts = getSortedPosts(posts);
return rss({
title: SITE.title,
description: SITE.desc,
site: SITE.website,
items: sortedPosts.map(({ data, id }) => ({
link: `posts/${id}/`,
title: data.title,
description: data.description,
pubDate: new Date(data.modDatetime ?? data.pubDatetime),
})),
});
}

132
src/pages/search.astro Normal file
View File

@ -0,0 +1,132 @@
---
import "@pagefind/default-ui/css/ui.css";
import Main from "@/layouts/Main.astro";
import Layout from "@/layouts/Layout.astro";
import Header from "@/components/Header.astro";
import Footer from "@/components/Footer.astro";
import { SITE } from "@/config";
const backUrl = SITE.showBackButton ? `${Astro.url.pathname}` : "/";
---
<Layout title={`Search | ${SITE.title}`}>
<Header />
<Main pageTitle="搜索" pageDesc="搜索所有文章 ...">
<div id="pagefind-search" transition:persist data-backurl={backUrl}></div>
</Main>
<Footer />
</Layout>
<script>
function initSearch() {
const pageFindSearch: HTMLElement | null =
document.querySelector("#pagefind-search");
if (!pageFindSearch) return;
const params = new URLSearchParams(window.location.search);
const onIdle = window.requestIdleCallback || (cb => setTimeout(cb, 1));
onIdle(async () => {
// @ts-expect-error — Missing types for @pagefind/default-ui package.
const { PagefindUI } = await import("@pagefind/default-ui");
// Display warning inn dev mode
if (import.meta.env.DEV) {
pageFindSearch.innerHTML = `
<div class="bg-muted/75 rounded p-4 space-y-4 mb-4">
<p><strong>开发模式警告!</strong>在开发期间,您需要至少构建一次项目才能看到搜索结果。</p>
<code class="block bg-black text-white px-2 py-1 rounded">pnpm run build</code>
</div>
`;
}
// Init pagefind ui
const search = new PagefindUI({
element: "#pagefind-search",
showSubResults: true,
showImages: false,
processTerm: function (term: string) {
params.set("q", term); // Update the `q` parameter in the URL
history.replaceState(history.state, "", "?" + params.toString()); // Push the new URL without reloading
const backUrl = pageFindSearch?.dataset?.backurl;
sessionStorage.setItem("backUrl", backUrl + "?" + params.toString());
return term;
},
});
// If search param exists (eg: search?q=astro), trigger search
const query = params.get("q");
if (query) {
search.triggerSearch(query);
}
// Reset search param if search input is cleared
const searchInput = document.querySelector(".pagefind-ui__search-input");
const clearButton = document.querySelector(".pagefind-ui__search-clear");
searchInput?.addEventListener("input", resetSearchParam);
clearButton?.addEventListener("click", resetSearchParam);
function resetSearchParam(e: Event) {
if ((e.target as HTMLInputElement)?.value.trim() === "") {
history.replaceState(history.state, "", window.location.pathname);
}
}
});
}
document.addEventListener("astro:after-swap", initSearch);
initSearch();
</script>
<style is:global>
#pagefind-search {
--pagefind-ui-font: var(--font-mono);
--pagefind-ui-text: var(--foreground);
--pagefind-ui-background: var(--background);
--pagefind-ui-border: var(--border);
--pagefind-ui-primary: var(--accent);
--pagefind-ui-tag: var(--background);
--pagefind-ui-border-radius: 0.375rem;
--pagefind-ui-border-width: 1px;
--pagefind-ui-image-border-radius: 8px;
--pagefind-ui-image-box-ratio: 3 / 2;
form::before {
background-color: var(--foreground);
}
input {
font-weight: 400;
border: 1px solid var(--border);
}
input:focus-visible {
outline: 1px solid var(--accent);
}
.pagefind-ui__result-title a {
color: var(--accent);
outline-offset: 1px;
outline-color: var(--accent);
}
.pagefind-ui__result-title a:focus-visible,
.pagefind-ui__search-clear:focus-visible {
text-decoration-line: none;
outline-width: 2px;
outline-style: dashed;
}
.pagefind-ui__result:last-of-type {
border-bottom: 0;
}
.pagefind-ui__result-nested .pagefind-ui__result-link:before {
font-family: system-ui;
}
}
</style>

View File

@ -0,0 +1,54 @@
---
import { getCollection } from "astro:content";
import type { GetStaticPathsOptions } from "astro";
import Main from "@/layouts/Main.astro";
import Layout from "@/layouts/Layout.astro";
import Header from "@/components/Header.astro";
import Footer from "@/components/Footer.astro";
import Card from "@/components/Card.astro";
import Pagination from "@/components/Pagination.astro";
import getUniqueTags from "@/utils/getUniqueTags";
import getPostsByTag from "@/utils/getPostsByTag";
import { SITE } from "@/config";
export async function getStaticPaths({ paginate }: GetStaticPathsOptions) {
const posts = await getCollection("blog");
const tags = getUniqueTags(posts);
return tags.flatMap(({ tag, tagName }) => {
const tagPosts = getPostsByTag(posts, tag);
return paginate(tagPosts, {
params: { tag },
props: { tagName },
pageSize: SITE.postPerPage,
});
});
}
const params = Astro.params;
const { tag } = params;
const { page, tagName } = Astro.props;
---
<Layout title={`Tag: ${tagName} | ${SITE.title}`}>
<Header />
<Main
pageTitle={[`标签:`, `${tagName}`]}
titleTransition={tag}
pageDesc={`所有含"${tagName}"标签的文章.`}
>
<h1 slot="title" transition:name={tag}>{`Tag:${tag}`}</h1>
<ul>
{
page.data.map(({ data, id }) => (
<Card href={`/posts/${id}`} frontmatter={data} />
))
}
</ul>
</Main>
<Pagination {page} />
<Footer noMarginTop={page.lastPage > 1} />
</Layout>

View File

@ -0,0 +1,24 @@
---
import { getCollection } from "astro:content";
import Main from "@/layouts/Main.astro";
import Layout from "@/layouts/Layout.astro";
import Tag from "@/components/Tag.astro";
import Header from "@/components/Header.astro";
import Footer from "@/components/Footer.astro";
import getUniqueTags from "@/utils/getUniqueTags";
import { SITE } from "@/config";
const posts = await getCollection("blog");
let tags = getUniqueTags(posts);
---
<Layout title={`标签 | ${SITE.title}`}>
<Header />
<Main pageTitle="标签" pageDesc="文章中所有用到的标签">
<ul>
{tags.map(({ tag, tagName }) => <Tag {tag} {tagName} size="lg" />)}
</ul>
</Main>
<Footer />
</Layout>

60
src/styles/global.css Normal file
View File

@ -0,0 +1,60 @@
@import "tailwindcss";
@import "./typography.css";
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
:root,
html[data-theme="light"] {
--background: #fdfdfd;
--foreground: #282728;
--accent: #006cac;
--muted: #e6e6e6;
--border: #ece9e9;
}
html[data-theme="dark"] {
--background: #212737;
--foreground: #eaedf3;
--accent: #ff6b01;
--muted: #343f60bf;
--border: #ab4b08;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-accent: var(--accent);
--color-muted: var(--muted);
--color-border: var(--border);
}
@layer base {
* {
@apply border-border outline-accent/75;
scrollbar-width: auto;
scrollbar-color: var(--color-muted) transparent;
}
html {
@apply overflow-y-scroll scroll-smooth;
}
body {
@apply flex min-h-svh flex-col bg-background font-sans text-foreground selection:bg-accent/75 selection:text-background;
font-family: system-ui, -apple-system, sans-serif;
}
a,
button {
@apply outline-offset-1 outline-accent focus-visible:no-underline focus-visible:outline-2 focus-visible:outline-dashed;
}
button:not(:disabled),
[role="button"]:not(:disabled) {
cursor: pointer;
}
section,
footer {
@apply mx-auto max-w-3xl px-4;
}
}
.active-nav {
@apply underline decoration-wavy decoration-2 underline-offset-4;
}

50
src/styles/typography.css Normal file
View File

@ -0,0 +1,50 @@
@plugin '@tailwindcss/typography';
@layer base {
.prose {
@apply prose-headings:!mb-3 prose-headings:!text-foreground prose-h3:italic prose-p:!text-foreground prose-a:!text-foreground prose-a:!decoration-dashed prose-a:underline-offset-8 hover:prose-a:text-accent prose-blockquote:!border-l-accent/50 prose-blockquote:opacity-80 prose-figcaption:!text-foreground prose-figcaption:opacity-70 prose-strong:!text-foreground prose-code:rounded prose-code:bg-muted/75 prose-code:p-1 prose-code:!text-foreground prose-code:before:!content-none prose-code:after:!content-none prose-ol:!text-foreground prose-ul:overflow-x-clip prose-ul:!text-foreground prose-li:marker:!text-accent prose-table:text-foreground prose-th:border prose-th:border-border prose-td:border prose-td:border-border prose-img:mx-auto prose-img:!my-2 prose-img:border-2 prose-img:border-border prose-hr:!border-border;
}
.prose a {
@apply break-words hover:!text-accent;
}
.prose thead th:first-child,
tbody td:first-child,
tfoot td:first-child {
padding-inline-start: 0.5714286em !important;
}
.prose h2#table-of-contents {
@apply mb-2;
}
.prose details {
@apply inline-block cursor-pointer text-foreground select-none;
}
.prose summary {
@apply focus-visible:no-underline focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-accent focus-visible:outline-dashed;
}
.prose h2#table-of-contents + p {
@apply hidden;
}
/* ===== Code Blocks & Syntax Highlighting ===== */
pre:has(code) {
@apply border border-border;
}
code,
blockquote {
word-wrap: break-word;
}
pre > code {
white-space: pre;
}
/* Apply Dark Theme (if multi-theme specified) */
html[data-theme="dark"] pre:has(code),
html[data-theme="dark"] pre:has(code) span {
color: var(--shiki-dark) !important;
background-color: var(--shiki-dark-bg) !important;
font-style: var(--shiki-dark-font-style) !important;
font-weight: var(--shiki-dark-font-weight) !important;
text-decoration: var(--shiki-dark-text-decoration) !important;
}
}

Some files were not shown because too many files have changed in this diff Show More