Initial commit from Astro
8
.dockerignore
Normal 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
|
@ -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
|
@ -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.
|
||||||
|
|
||||||
|
Here’s 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 don’t find any open issue that addresses the bug/problem you’re 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, we’d 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 don’t 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 you’re unsure whether your idea is feasible or fits the project’s 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, it’s 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 doesn’t currently have automated testing, it’s especially useful if you can do manual testing on open PRs and provide feedback.
|
||||||
|
|
||||||
|
## Starting a Discussion
|
||||||
|
|
||||||
|
If you’re 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). It’s a great way to engage with the community.
|
2
.github/FUNDING.yml
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
github: [satnaing]
|
||||||
|
buy_me_a_coffee: satnaing
|
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal 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.
|
19
.github/ISSUE_TEMPLATE/✨-feature-request.md
vendored
Normal 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.
|
27
.github/ISSUE_TEMPLATE/🐞-bug-report.md
vendored
Normal 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.
|
16
.github/ISSUE_TEMPLATE/📝-documentation-improvement.md
vendored
Normal 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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"recommendations": ["astro-build.astro-vscode"],
|
||||||
|
"unwantedRecommendations": []
|
||||||
|
}
|
11
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"command": "./node_modules/.bin/astro dev",
|
||||||
|
"name": "Development server",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "node-terminal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
159
AstroPaper-lighthouse-score.svg
Normal 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 |
17
Dockerfile
Normal 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
|
@ -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
|
@ -0,0 +1,177 @@
|
||||||
|
# AstroPaper 📄
|
||||||
|
|
||||||
|

|
||||||
|
[](https://www.figma.com/community/file/1356898632249991861)
|
||||||
|

|
||||||
|

|
||||||
|
[](https://conventionalcommits.org)
|
||||||
|
[](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
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm 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 pnpm 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).
|
40
astro.config.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
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,
|
||||||
|
preserveScriptOrder: true,
|
||||||
|
},
|
||||||
|
});
|
7
cz.yaml
Normal 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
|
9
docker-compose.yml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: node:lts
|
||||||
|
ports:
|
||||||
|
- 4321:4321
|
||||||
|
working_dir: /app
|
||||||
|
command: npm run dev -- --host 0.0.0.0
|
||||||
|
volumes:
|
||||||
|
- ./:/app
|
18
eslint.config.js
Normal 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/**"] },
|
||||||
|
];
|
45
package.json
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"type": "module",
|
||||||
|
"version": "5.2.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.14",
|
||||||
|
"astro": "^5.5.2",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
|
"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.14"
|
||||||
|
},
|
||||||
|
"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.1",
|
||||||
|
"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.1"
|
||||||
|
}
|
||||||
|
}
|
5462
pnpm-lock.yaml
Normal file
BIN
public/assets/forrest-gump-quote.webp
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
public/astropaper-og.jpg
Normal file
After Width: | Height: | Size: 145 KiB |
361
public/dev.svg
Normal 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
|
@ -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
|
@ -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
|
@ -0,0 +1 @@
|
||||||
|
declare module 'remark-collapse';
|
1
src/assets/icons/IconArchive.svg
Normal 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 |
1
src/assets/icons/IconArrowLeft.svg
Normal 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 |
1
src/assets/icons/IconArrowRight.svg
Normal 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 |
1
src/assets/icons/IconBrandX.svg
Normal 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 |
1
src/assets/icons/IconCalendar.svg
Normal 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-calendar-week"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 7a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12z" /><path d="M16 3v4" /><path d="M8 3v4" /><path d="M4 11h16" /><path d="M7 14h.013" /><path d="M10.01 14h.005" /><path d="M13.01 14h.005" /><path d="M16.015 14h.005" /><path d="M13.015 17h.005" /><path d="M7.01 17h.005" /><path d="M10.01 17h.005" /></svg>
|
After Width: | Height: | Size: 658 B |
1
src/assets/icons/IconChevronLeft.svg
Normal 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 |
1
src/assets/icons/IconChevronRight.svg
Normal 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 |
1
src/assets/icons/IconEdit.svg
Normal 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 |
1
src/assets/icons/IconFacebook.svg
Normal 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 |
1
src/assets/icons/IconGitHub.svg
Normal 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 |
1
src/assets/icons/IconHash.svg
Normal 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 |
1
src/assets/icons/IconLinkedin.svg
Normal 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 |
1
src/assets/icons/IconMail.svg
Normal 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 |
1
src/assets/icons/IconMenuDeep.svg
Normal 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 |
1
src/assets/icons/IconMoon.svg
Normal 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 |
1
src/assets/icons/IconPinterest.svg
Normal 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 |
1
src/assets/icons/IconRss.svg
Normal 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 |
1
src/assets/icons/IconSearch.svg
Normal 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 |
1
src/assets/icons/IconSun.svg
Normal 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 |
1
src/assets/icons/IconSunHigh.svg
Normal 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 |
1
src/assets/icons/IconTelegram.svg
Normal 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 |
1
src/assets/icons/IconWhatsapp.svg
Normal 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 |
1
src/assets/icons/IconX.svg
Normal 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 |
BIN
src/assets/images/AstroPaper-v3.png
Normal file
After Width: | Height: | Size: 169 KiB |
BIN
src/assets/images/AstroPaper-v4.png
Normal file
After Width: | Height: | Size: 158 KiB |
BIN
src/assets/images/AstroPaper-v5.png
Normal file
After Width: | Height: | Size: 174 KiB |
BIN
src/assets/images/forrest-gump-quote.png
Normal file
After Width: | Height: | Size: 275 KiB |
37
src/components/BackButton.astro
Normal 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>Go back</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>
|
57
src/components/Breadcrumb.astro
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
---
|
||||||
|
// 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, `Posts (page ${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 ? "" : "(page " + breadcrumbList[2] + ")"}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<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">Home</a>
|
||||||
|
<span aria-hidden="true" class="opacity-80">»</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">»</span>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
36
src/components/Card.astro
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
---
|
||||||
|
import { slugifyStr } from "@/utils/slugify";
|
||||||
|
import type { CollectionEntry } from "astro:content";
|
||||||
|
import { getPath } from "@/utils/getPath";
|
||||||
|
import Datetime from "./Datetime.astro";
|
||||||
|
|
||||||
|
export interface Props extends CollectionEntry<"blog"> {
|
||||||
|
variant?: "h2" | "h3";
|
||||||
|
}
|
||||||
|
|
||||||
|
const { variant = "h2", data, id, filePath } = Astro.props;
|
||||||
|
|
||||||
|
const { title, description, pubDatetime, modDatetime, timezone } = data;
|
||||||
|
|
||||||
|
const headerProps = {
|
||||||
|
style: { viewTransitionName: slugifyStr(title) },
|
||||||
|
class: "text-lg font-medium decoration-dashed hover:underline",
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<li class="my-6">
|
||||||
|
<a
|
||||||
|
href={getPath(id, filePath)}
|
||||||
|
class="inline-block text-lg font-medium text-accent decoration-dashed underline-offset-4 focus-visible:no-underline focus-visible:underline-offset-0"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
variant === "h2" ? (
|
||||||
|
<h2 {...headerProps}>{title}</h2>
|
||||||
|
) : (
|
||||||
|
<h3 {...headerProps}>{title}</h3>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</a>
|
||||||
|
<Datetime {pubDatetime} {modDatetime} {timezone} />
|
||||||
|
<p>{description}</p>
|
||||||
|
</li>
|
58
src/components/Datetime.astro
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
---
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import utc from "dayjs/plugin/utc";
|
||||||
|
import timezone from "dayjs/plugin/timezone";
|
||||||
|
import IconCalendar from "@/assets/icons/IconCalendar.svg";
|
||||||
|
import { SITE } from "@/config";
|
||||||
|
|
||||||
|
dayjs.extend(utc);
|
||||||
|
dayjs.extend(timezone);
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
class?: string;
|
||||||
|
size?: "sm" | "lg";
|
||||||
|
timezone: string | undefined;
|
||||||
|
pubDatetime: string | Date;
|
||||||
|
modDatetime: string | Date | undefined | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
pubDatetime,
|
||||||
|
modDatetime,
|
||||||
|
size = "sm",
|
||||||
|
class: className = "",
|
||||||
|
timezone: postTimezone,
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
/* ========== Formatted Datetime ========== */
|
||||||
|
const latestDatetime =
|
||||||
|
modDatetime && modDatetime > pubDatetime ? modDatetime : pubDatetime;
|
||||||
|
const datetime = dayjs(latestDatetime).tz(postTimezone || SITE.timezone);
|
||||||
|
|
||||||
|
const date = datetime.format("D MMM, YYYY"); // e.g., '22 Mar, 2025'
|
||||||
|
const time = datetime.format("hh:mm A"); // e.g., '08:30 PM'
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class:list={["flex items-end space-x-2 opacity-80", className]}>
|
||||||
|
<IconCalendar
|
||||||
|
class:list={[
|
||||||
|
"inline-block size-6 min-w-[1.375rem]",
|
||||||
|
{ "scale-90": size === "sm" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
{
|
||||||
|
modDatetime && modDatetime > pubDatetime ? (
|
||||||
|
<span class:list={["text-sm italic", { "sm:text-base": size === "lg" }]}>
|
||||||
|
Updated:
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span class="sr-only">Published:</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<span class:list={["text-sm italic", { "sm:text-base": size === "lg" }]}>
|
||||||
|
<time datetime={datetime.toISOString()}>{date}</time>
|
||||||
|
<span aria-hidden="true"> | </span>
|
||||||
|
<span class="sr-only"> at </span>
|
||||||
|
<span class="text-nowrap">{time}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
38
src/components/EditPost.astro
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
---
|
||||||
|
import type { CollectionEntry } from "astro:content";
|
||||||
|
import IconEdit from "@/assets/icons/IconEdit.svg";
|
||||||
|
import { SITE } from "@/config";
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
hideEditPost?: CollectionEntry<"blog">["data"]["hideEditPost"];
|
||||||
|
class?: string;
|
||||||
|
post: CollectionEntry<"blog">;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { hideEditPost, post, class: className = "" } = Astro.props;
|
||||||
|
|
||||||
|
const href = `${SITE.editPost.url}${post.filePath}`;
|
||||||
|
const showEditPost =
|
||||||
|
SITE.editPost.enabled && !hideEditPost && href.trim() !== "";
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
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={href}
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<IconEdit class="inline-block size-6" />
|
||||||
|
<span class="italic max-sm:text-sm sm:inline">
|
||||||
|
{SITE.editPost.text}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
26
src/components/Footer.astro
Normal 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 © {currentYear}</span>
|
||||||
|
<span class="hidden sm:inline"> | </span>
|
||||||
|
<span>All rights reserved.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
168
src/components/Header.astro
Normal 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:items-center 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") }}>
|
||||||
|
Posts
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="col-span-2">
|
||||||
|
<a href="/tags" class:list={{ "active-nav": isActive("/tags") }}>
|
||||||
|
Tags
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="col-span-2">
|
||||||
|
<a href="/about" class:list={{ "active-nav": isActive("/about") }}>
|
||||||
|
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="Archives"
|
||||||
|
>
|
||||||
|
<IconArchive class="hidden sm:inline-block" />
|
||||||
|
<span class="sm:sr-only">Archives</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="Search"
|
||||||
|
>
|
||||||
|
<IconSearch />
|
||||||
|
<span class="sr-only">Search</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="Toggles light & dark"
|
||||||
|
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
|
@ -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>
|
42
src/components/LinkButton.astro
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
39
src/components/Pagination.astro
Normal 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"
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
<IconArrowRight class="inline-block" />
|
||||||
|
</LinkButton>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
26
src/components/ShareLinks.astro
Normal 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">Share this post on:</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>
|
25
src/components/Socials.astro
Normal 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>
|
36
src/components/Tag.astro
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
---
|
||||||
|
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" },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<IconHash
|
||||||
|
class:list={[
|
||||||
|
"inline-block opacity-80",
|
||||||
|
{ "-mr-3.5 size-4": size === "sm" },
|
||||||
|
{ "-mr-5 size-6": size === "lg" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<span>{tagName}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
22
src/config.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
export const SITE = {
|
||||||
|
website: "https://astro-paper.pages.dev/", // replace this with your deployed domain
|
||||||
|
author: "Sat Naing",
|
||||||
|
profile: "https://satnaing.dev/",
|
||||||
|
desc: "A minimal, responsive and SEO-friendly Astro blog theme.",
|
||||||
|
title: "AstroPaper",
|
||||||
|
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: {
|
||||||
|
enabled: true,
|
||||||
|
text: "Suggest Changes",
|
||||||
|
url: "https://github.com/satnaing/astro-paper/edit/main/",
|
||||||
|
},
|
||||||
|
dynamicOgImage: true,
|
||||||
|
lang: "en", // html lang code. Set this empty and default will be "en"
|
||||||
|
timezone: "Asia/Bangkok", // Default global timezone (IANA format) https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||||
|
} as const;
|
75
src/constants.ts
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
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 SOCIALS = [
|
||||||
|
{
|
||||||
|
name: "Github",
|
||||||
|
href: "https://github.com/satnaing/astro-paper",
|
||||||
|
linkTitle: ` ${SITE.title} on Github`,
|
||||||
|
icon: IconGitHub,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "X",
|
||||||
|
href: "https://x.com/username",
|
||||||
|
linkTitle: `${SITE.title} on X`,
|
||||||
|
icon: IconBrandX,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "LinkedIn",
|
||||||
|
href: "https://www.linkedin.com/in/username/",
|
||||||
|
linkTitle: `${SITE.title} on LinkedIn`,
|
||||||
|
icon: IconLinkedin,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Mail",
|
||||||
|
href: "mailto:yourmail@gmail.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;
|
26
src/content.config.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { defineCollection, z } from "astro:content";
|
||||||
|
import { glob } from "astro/loaders";
|
||||||
|
import { SITE } from "@/config";
|
||||||
|
|
||||||
|
export const BLOG_PATH = "src/data/blog";
|
||||||
|
|
||||||
|
const blog = defineCollection({
|
||||||
|
loader: glob({ pattern: "**/[^_]*.md", base: `./${BLOG_PATH}` }),
|
||||||
|
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(),
|
||||||
|
hideEditPost: z.boolean().optional(),
|
||||||
|
timezone: z.string().optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const collections = { blog };
|
91
src/data/blog/_releases/astro-paper-2.md
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
---
|
||||||
|
author: Sat Naing
|
||||||
|
pubDatetime: 2023-01-30T15:57:52.737Z
|
||||||
|
title: AstroPaper 2.0
|
||||||
|
slug: astro-paper-2
|
||||||
|
featured: false
|
||||||
|
ogImage: https://user-images.githubusercontent.com/53733092/215771435-25408246-2309-4f8b-a781-1f3d93bdf0ec.png
|
||||||
|
tags:
|
||||||
|
- release
|
||||||
|
description: AstroPaper with the enhancements of Astro v2. Type-safe markdown contents, bug fixes and better dev experience etc.
|
||||||
|
---
|
||||||
|
|
||||||
|
Astro 2.0 has been released with some cool features, breaking changes, DX improvements, better error overlay and so on. AstroPaper takes advantage of those cool features, especially Content Collections API.
|
||||||
|
|
||||||
|
<!--  -->
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Table of contents
|
||||||
|
|
||||||
|
## Features & Changes
|
||||||
|
|
||||||
|
### Type-safe Frontmatters and Redefined Blog Schema
|
||||||
|
|
||||||
|
Frontmatter of AstroPaper 2.0 markdown contents are now type-safe thanks to Astro’s Content Collections. Blog schema is defined inside the `src/content/_schemas.ts` file.
|
||||||
|
|
||||||
|
### New Home for Blog contents
|
||||||
|
|
||||||
|
All the blog posts were moved from `src/contents` to `src/content/blog` directory.
|
||||||
|
|
||||||
|
### New Fetch API
|
||||||
|
|
||||||
|
Contents are now fetched with `getCollection` function. No relative path to the content needs to be specified anymore.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// old content fetching method
|
||||||
|
- const postImportResult = import.meta.glob<MarkdownInstance<Frontmatter>>(
|
||||||
|
"../contents/**/**/*.md",);
|
||||||
|
|
||||||
|
// new content fetching method
|
||||||
|
+ const postImportResult = await getCollection("blog");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Modified Search Logic for better Search Result
|
||||||
|
|
||||||
|
In the older version of AstroPaper, when someone search some article, the search criteria keys that will be searched are `title`, `description` and `headings` (heading means all the headings h1 ~ h6 of the blog post). In AstroPaper v2, only `title` and `description` will be searched as the user types.
|
||||||
|
|
||||||
|
### Renamed Frontmatter Properties
|
||||||
|
|
||||||
|
The following frontmatter properties are renamed.
|
||||||
|
|
||||||
|
| Old Names | New Names |
|
||||||
|
| --------- | ----------- |
|
||||||
|
| datetime | pubDatetime |
|
||||||
|
| slug | postSlug |
|
||||||
|
|
||||||
|
### Default Tag for blog post
|
||||||
|
|
||||||
|
If a blog post doesn't have any tag (in other words, frontmatter property `tags` is not specified), the default tag `others` will be used for that blog post. But you can set the default tag in the `/src/content/_schemas.ts` file.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/contents/_schemas.ts
|
||||||
|
export const blogSchema = z.object({
|
||||||
|
// ---
|
||||||
|
// replace "others" with whatever you want
|
||||||
|
tags: z.array(z.string()).default(["others"]),
|
||||||
|
ogImage: z.string().optional(),
|
||||||
|
description: z.string(),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### New Predefined Dark Color Scheme
|
||||||
|
|
||||||
|
AstroPaper v2 has a new dark color scheme (high contrast & low contrast) which is based on Astro's dark logo. Check out [this link](https://astro-paper.pages.dev/posts/predefined-color-schemes#astro-dark) for more info.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Automatic Class Sorting
|
||||||
|
|
||||||
|
AstroPaper 2.0 includes automatic class sorting with [TailwindCSS Prettier plugin](https://tailwindcss.com/blog/automatic-class-sorting-with-prettier)
|
||||||
|
|
||||||
|
### Updated Docs & README
|
||||||
|
|
||||||
|
All the [#docs](https://astro-paper.pages.dev/tags/docs/) blog posts and [README](https://github.com/satnaing/astro-paper#readme) are updated for this AstroPaper v2.
|
||||||
|
|
||||||
|
## Bug Fixes
|
||||||
|
|
||||||
|
- fix broken tags in the Blog Post page
|
||||||
|
- in a tag page, the last part of the breadcrumb is now updated to lower-case for consistency
|
||||||
|
- exclude draft posts in a tag page
|
||||||
|
- fix 'onChange value not updating issue' after a page reload
|
173
src/data/blog/_releases/astro-paper-3.md
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
---
|
||||||
|
author: Sat Naing
|
||||||
|
pubDatetime: 2023-09-25T10:25:54.547Z
|
||||||
|
title: AstroPaper 3.0
|
||||||
|
slug: astro-paper-v3
|
||||||
|
featured: false
|
||||||
|
ogImage: https://github.com/satnaing/astro-paper/assets/53733092/1ef0cf03-8137-4d67-ac81-84a032119e3a
|
||||||
|
tags:
|
||||||
|
- release
|
||||||
|
description: "AstroPaper Version 3: Elevating Your Web Experience with Astro v3 and Seamless View Transitions"
|
||||||
|
---
|
||||||
|
|
||||||
|
We're excited to announce the release of AstroPaper v3, packed with new features, enhancements, and bug fixes to elevate your web development experience. Let's dive into the highlights of this release:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Table of contents
|
||||||
|
|
||||||
|
## Features & Changes
|
||||||
|
|
||||||
|
### Astro v3 Integration
|
||||||
|
|
||||||
|
<video autoplay loop="loop" muted="muted" plays-inline="true">
|
||||||
|
<source src="https://github.com/satnaing/astro-paper/assets/53733092/18fdb604-1ca3-41a0-8372-1367759091ff" type="video/mp4">
|
||||||
|
<!-- <source src="/assets/docs/astro-paper-v3-view-transitions-demo.mp4" type="video/mp4"> -->
|
||||||
|
</video>
|
||||||
|
|
||||||
|
AstroPaper now fully supports [Astro v3](https://astro.build/blog/astro-3/), offering improved performance and rendering speed.
|
||||||
|
|
||||||
|
Besides, we've added support for Astro's [ViewTransitions API](https://docs.astro.build/en/guides/view-transitions/), allowing you to create captivating and dynamic transitions between views.
|
||||||
|
|
||||||
|
In the "Recent Section", only non-featured posts will be displayed to avoid duplications and better support for ViewTransitions API.
|
||||||
|
|
||||||
|
### Update OG Image Generation Logic
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
We've updated the logic for automatic OG image generation, making it even more reliable and efficient. Besides, it now supports special characters in post titles, ensuring accurate, flexible and eye-catching social media previews.
|
||||||
|
|
||||||
|
`SITE.ogImage` is now optional. If it is not specified, AstroPaper will automatically generate an OG image using `SITE.title`, `SITE.desc` and `SITE.website`
|
||||||
|
|
||||||
|
### Theme meta tag
|
||||||
|
|
||||||
|
The theme-color meta tag has been added to dynamically adapt to theme switches, ensuring a seamless user experience.
|
||||||
|
|
||||||
|
> Notice the difference at the top
|
||||||
|
|
||||||
|
**_AstroPaper v2 theme switch_**
|
||||||
|
|
||||||
|
<video autoplay loop="loop" muted="muted" plays-inline="true">
|
||||||
|
<source src="https://github.com/satnaing/astro-paper/assets/53733092/3ab5a1e8-1891-4264-a5bb-0ded69143c1a" type="video/mp4">
|
||||||
|
</video>
|
||||||
|
|
||||||
|
**_AstroPaper v3 theme switch_**
|
||||||
|
|
||||||
|
<video autoplay loop="loop" muted="muted" plays-inline="true">
|
||||||
|
<source src="https://github.com/satnaing/astro-paper/assets/53733092/8ac9deb8-d1f8-4029-86bd-6aa0def380b4" type="video/mp4">
|
||||||
|
</video>
|
||||||
|
|
||||||
|
## Other Changes
|
||||||
|
|
||||||
|
### Astro Prettier Plugin
|
||||||
|
|
||||||
|
Astro Prettier Plugin is installed out-of-the-box in order to keep the project tidy and organized.
|
||||||
|
|
||||||
|
### Minor Style Changes
|
||||||
|
|
||||||
|
The single-line code block wrapping issue has been solved, making your code snippets look pristine.
|
||||||
|
|
||||||
|
Update nav style CSS to allow adding more nav links to the navigation.
|
||||||
|
|
||||||
|
## Upgrade to AstroPaper v3
|
||||||
|
|
||||||
|
> This section is only for those who want to upgrade AstroPaper v3 from the older versions.
|
||||||
|
|
||||||
|
This section will help you migrate from AstroPaper v2 to AstroPaper v3.
|
||||||
|
|
||||||
|
Before reading the rest of the section, you might also want to check [this article](https://astro-paper.pages.dev/posts/how-to-update-dependencies/) for upgrading dependencies and AstroPaper.
|
||||||
|
|
||||||
|
## Option 1: Fresh Restart (recommended)
|
||||||
|
|
||||||
|
In this release, a lot of changes have been made\_ replacing old Astro APIs with newer APIs, bug fixes, new features etc. Thus, if you are someone who didn't make customization very much, you should follow this approach.
|
||||||
|
|
||||||
|
**_Step 1: Keep all your updated files_**
|
||||||
|
|
||||||
|
It's important to keep all the files which have been already updated. These files include
|
||||||
|
|
||||||
|
- `/src/config.ts` (didn't touch in v3)
|
||||||
|
- `/src/styles/base.css` (minor changes in v3; mentioned below)
|
||||||
|
- `/src/assets/` (didn't touch in v3)
|
||||||
|
- `/public/assets/` (didn't touch in v3)
|
||||||
|
- `/content/blog/` (it's your blog content directory 🤷🏻♂️)
|
||||||
|
- Any other customizations you've made.
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* file: /src/styles/base.css */
|
||||||
|
@layer base {
|
||||||
|
/* Other Codes */
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
@apply bg-skin-card-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Old code
|
||||||
|
code {
|
||||||
|
white-space: pre;
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* New code */
|
||||||
|
code,
|
||||||
|
blockquote {
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
pre > code {
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
/* other codes */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**_Step 1: Replace everything else with AstroPaper v3_**
|
||||||
|
|
||||||
|
In this step, replace everything\_ except above files/directories (plus your customized files/directories)\_ with AstroPaper v3.
|
||||||
|
|
||||||
|
**_Step 3: Schema Updates_**
|
||||||
|
|
||||||
|
Keep in mind that `/src/content/_schemas.ts` has been replaced with `/src/content/config.ts`.
|
||||||
|
|
||||||
|
Besides, there is no longer `BlogFrontmatter` type exported from `/src/content/config.ts`.
|
||||||
|
|
||||||
|
Therefore, all the `BlogFrontmatter` type inside files need to be updated with `CollectionEntry<"blog">["data"]`.
|
||||||
|
|
||||||
|
For example: `src/components/Card.tsx`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// AstroPaper v2
|
||||||
|
import type { BlogFrontmatter } from "@content/_schemas";
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
href?: string;
|
||||||
|
frontmatter: BlogFrontmatter;
|
||||||
|
secHeading?: boolean;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// AstroPaper v3
|
||||||
|
import type { CollectionEntry } from "astro:content";
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
href?: string;
|
||||||
|
frontmatter: CollectionEntry<"blog">["data"];
|
||||||
|
secHeading?: boolean;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Option 2: Upgrade using Git
|
||||||
|
|
||||||
|
This approach is not recommended for most users. You should do the "Option 1" if you can. Only do this if you know how to resolve merge conflicts and you know what you're doing.
|
||||||
|
|
||||||
|
Actually, I've already written a blog post for this case and you can check out [here](https://astro-paper.pages.dev/posts/how-to-update-dependencies/#updating-astropaper-using-git).
|
||||||
|
|
||||||
|
## Outro
|
||||||
|
|
||||||
|
Ready to explore the exciting new features and improvements in AstroPaper v3? Start [using AstroPaper](https://github.com/satnaing/astro-paper) now.
|
||||||
|
|
||||||
|
For other bug fixes and integration updates, check out the [release notes](https://github.com/satnaing/astro-paper/releases/tag/v3.0.0) to learn more.
|
||||||
|
|
||||||
|
If you encounter any bugs or face difficulties during the upgrade process, please feel free to open an issue or start a discussion on [GitHub](https://github.com/satnaing/astro-paper).
|
124
src/data/blog/_releases/astro-paper-4.md
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
---
|
||||||
|
author: Sat Naing
|
||||||
|
pubDatetime: 2024-01-04T09:30:41.816Z
|
||||||
|
title: AstroPaper 4.0
|
||||||
|
slug: "astro-paper-v4"
|
||||||
|
featured: false
|
||||||
|
ogImage: ../../../assets/images/AstroPaper-v4.png
|
||||||
|
tags:
|
||||||
|
- release
|
||||||
|
description: "AstroPaper v4: ensuring a smoother and more feature-rich blogging experience."
|
||||||
|
---
|
||||||
|
|
||||||
|
Hello everyone! Wishing you a happy New Year 🎉 and all the best for 2024! We're excited to announce the release of AstroPaper v4, a significant update that introduces a range of new features, improvements, and bug fixes to elevate your blogging experience. A big thank you to all the contributors for their valuable input and efforts in making version 4 possible!
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Table of contents
|
||||||
|
|
||||||
|
## Major Changes
|
||||||
|
|
||||||
|
### Upgrade to Astro v4 [#202](https://github.com/satnaing/astro-paper/pull/202)
|
||||||
|
|
||||||
|
AstroPaper now leverages the power and capabilities of Astro v4. However, it’s a subtle upgrade and won’t break most Astro users.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Replace `postSlug` with Astro Content `slug` [#197](https://github.com/satnaing/astro-paper/pull/197)
|
||||||
|
|
||||||
|
The `postSlug` in the blog content schema is no longer available in AstroPaper v4. Initially Astro doesn't have a `slug` mechanism and thus we have to figure it out on our own. Since Astro v3, it supports content collection and slug features. Now, we believe it's time to adopt Astro's out-of-the-box `slug` feature.
|
||||||
|
|
||||||
|
**_file: src/content/blog/astro-paper-4.md_**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
---
|
||||||
|
author: Sat Naing
|
||||||
|
pubDatetime: 2024-01-01T04:35:33.428Z
|
||||||
|
title: AstroPaper 4.0
|
||||||
|
slug: "astro-paper-v4" # if slug is not specified, it will be 'astro-paper-4' (file name).
|
||||||
|
# slug: "" ❌ cannot be an empty string
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
The behavior of the `slug` is slightly different now. In the previous versions of AstroPaper, if the `postSlug` is not specified in a blog post (markdown file), the title of that blog post would be slugified and used as the `slug`. However, in AstroPaper v4, if the `slug` field is not specified, the markdown file name will be used as the `slug`. One thing to keep in mind is that the `slug` field can be omitted, but it cannot be an empty string (slug: "" ❌).
|
||||||
|
|
||||||
|
If you're upgrading AstroPaper from v3 to v4, make sure to replace `postSlug` in your `src/content/blog/*.md` files with `slug`.
|
||||||
|
|
||||||
|
## New Features
|
||||||
|
|
||||||
|
### Add code-snippets for content creation [#206](https://github.com/satnaing/astro-paper/pull/206)
|
||||||
|
|
||||||
|
AstroPaper now includes VSCode snippets for new blog posts, eliminating the need for manual copy/pasting of the frontmatter and content structure (table of contents, heading, excerpt, etc.).
|
||||||
|
|
||||||
|
Read more about VSCode Snippets [here](https://code.visualstudio.com/docs/editor/userdefinedsnippets#:~:text=In%20Visual%20Studio%20Code%2C%20snippets,Snippet%20in%20the%20Command%20Palette).
|
||||||
|
|
||||||
|
<video autoplay muted="muted" controls plays-inline="true" class="border border-skin-line">
|
||||||
|
<source src="https://github.com/satnaing/astro-paper/assets/53733092/136f1903-bade-40a2-b6bb-285a3c726350" type="video/mp4">
|
||||||
|
</video>
|
||||||
|
|
||||||
|
### Add Modified Datetime in Blog Posts [#195](https://github.com/satnaing/astro-paper/pull/195)
|
||||||
|
|
||||||
|
Keep readers informed about the latest updates by displaying the modified datetime in blog posts. This not only instills user trust in the freshness of the articles but also contributes to improved SEO for the blog.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
You can add a `modDatetime` to your blog post if you've made modifications. Now, the sorting behavior of the posts is slightly different. All posts are sorted by both `pubDatetime` and `modDatetime`. If a post has both a `pubDatetime` and `modDatetime`, its sorting position will be determined by the `modDatetime`. If not, only `pubDatetime` will be considered to determine the post's sorting order.
|
||||||
|
|
||||||
|
### Implement Back-to-Top Button [#188](https://github.com/satnaing/astro-paper/pull/188)
|
||||||
|
|
||||||
|
Enhance user navigation on your blog detail post with the newly implemented back-to-top button.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Add Pagination in Tag Posts [#201](https://github.com/satnaing/astro-paper/pull/201)
|
||||||
|
|
||||||
|
Improve content organization and navigation with the addition of pagination in tag posts, making it easier for users to explore related content. This ensures that if a tag has many posts, readers won't be overwhelmed by all the tag-related posts.
|
||||||
|
|
||||||
|
<video autoplay loop="loop" muted="muted" plays-inline="true" class="border border-skin-line">
|
||||||
|
<source src="https://github.com/satnaing/astro-paper/assets/53733092/9bad87f5-dcf5-4b79-b67a-d6c7244cd616" type="video/mp4">
|
||||||
|
</video>
|
||||||
|
|
||||||
|
### Dynamically Generate robots.txt [#130](https://github.com/satnaing/astro-paper/pull/130)
|
||||||
|
|
||||||
|
AstroPaper v4 now dynamically generates the robots.txt file, giving you more control over search engine indexing and web crawling. Besides, sitemap URL will also be added inside `robot.txt` file.
|
||||||
|
|
||||||
|
### Add Docker-Compose File [#174](https://github.com/satnaing/astro-paper/pull/174)
|
||||||
|
|
||||||
|
Managing your AstroPaper environment is now easier than ever with the addition of a Docker-Compose file, simplifying deployment and configuration.
|
||||||
|
|
||||||
|
## Refactoring & Bug Fixes
|
||||||
|
|
||||||
|
### Replace Slugified Title with Unslugified Tag Name [#198](https://github.com/satnaing/astro-paper/pull/198)
|
||||||
|
|
||||||
|
To improve clarity, user experience and SEO, titles (`Tag: some-tag`) in tag page are no longer slugified (`Tag: Some Tag`).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Implement 100svh for Min-Height ([79d569d](https://github.com/satnaing/astro-paper/commit/79d569d053036f2113519f41b0d257523d035b76))
|
||||||
|
|
||||||
|
We've updated the min-height on the body to use 100svh, offering a better UX for mobile users.
|
||||||
|
|
||||||
|
### Update Site URL as Single Source of Truth [#143](https://github.com/satnaing/astro-paper/pull/143)
|
||||||
|
|
||||||
|
The site URL is now a single source of truth, streamlining configuration and avoiding inconsistencies. Read more at this [PR](https://github.com/satnaing/astro-paper/pull/143) and its related issue(s).
|
||||||
|
|
||||||
|
### Solve Invisible Text Code Block Issue in Light Mode [#163](https://github.com/satnaing/astro-paper/pull/163)
|
||||||
|
|
||||||
|
We've fixed the invisible text code block issue in light mode.
|
||||||
|
|
||||||
|
### Decode Unicode Tag Characters in Breadcrumb [#175](https://github.com/satnaing/astro-paper/pull/175)
|
||||||
|
|
||||||
|
The last part of Tag in the breadcrumb is now decoded, making non-English Unicode characters display better.
|
||||||
|
|
||||||
|
### Update LOCALE Config to Cover Overall Locales ([cd02b04](https://github.com/satnaing/astro-paper/commit/cd02b047d2b5e3b4a2940c0ff30568cdebcec0b8))
|
||||||
|
|
||||||
|
The LOCALE configuration has been updated to cover a broader range of locales, catering to a more diverse audience.
|
||||||
|
|
||||||
|
## Outtro
|
||||||
|
|
||||||
|
We believe these updates will significantly elevate your AstroPaper experience. Thank you to everyone who contributed, solved issues, and gave stars to AstroPaper. We look forward to seeing the amazing content you create with AstroPaper v4!
|
||||||
|
|
||||||
|
Happy Blogging!
|
||||||
|
|
||||||
|
[Sat Naing](https://satnaing.dev) <br/>
|
||||||
|
Creator of AstroPaper
|
101
src/data/blog/_releases/astro-paper-5.md
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
---
|
||||||
|
pubDatetime: 2025-03-08T08:18:19.693Z
|
||||||
|
title: AstroPaper 5.0
|
||||||
|
slug: astro-paper-v5
|
||||||
|
featured: true
|
||||||
|
ogImage: ../../../assets/images/AstroPaper-v5.png
|
||||||
|
tags:
|
||||||
|
- release
|
||||||
|
description: "AstroPaper v5: keep the clean look, updates under the hood."
|
||||||
|
---
|
||||||
|
|
||||||
|
At last, the long-awaited AstroPaper v5 is finally here. AstroPaper v5 keeps the same minimal & clean look, but comes with significant updates under the hood.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Table of contents
|
||||||
|
|
||||||
|
## Major Changes
|
||||||
|
|
||||||
|
### Upgrade to Astro v5 [#455](https://github.com/satnaing/astro-paper/pull/455)
|
||||||
|
|
||||||
|
AstroPaper now comes with Astro v5, bringing all the new features and improvements that come with it.
|
||||||
|
|
||||||
|
### Tailwind v4
|
||||||
|
|
||||||
|
AstroPaper has been upgraded to Tailwind v4, which includes many style changes under the hood. The `tailwind.config.js` file has been removed, and now all the configuration is located within the `src/styles/global.css` file. Typography-related styles have been extracted and moved to `src/styles/typography.css`.
|
||||||
|
|
||||||
|
Due to the new behavior in TailwindCSS v4, styles inside `<style>` blocks within components have been removed and replaced with inline Tailwind classes.
|
||||||
|
|
||||||
|
Additionally, the color palette across the UI has been updated. The new palette now consists of only five colors:
|
||||||
|
|
||||||
|
```css
|
||||||
|
: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;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Remove React + Fuse.js in favor of Pagefind search
|
||||||
|
|
||||||
|
In previous versions, React.js and Fuse.js were used for search functionality and OG image generation. In AstroPaper v5, React.js has been removed and replaced with [Pagefind](https://pagefind.app/), a static site search tool.
|
||||||
|
|
||||||
|
The search experience is almost identical to previous versions, but now all contents, not just titles and descriptions, are indexed and searchable, thanks to Pagefind.
|
||||||
|
|
||||||
|
The idea of using Pagefind in dev mode was inspired by [this blog post](https://chrispennington.blog/blog/pagefind-static-search-for-astro-sites/).
|
||||||
|
|
||||||
|
### Updated import alias
|
||||||
|
|
||||||
|
The import alias has been updated from `@directory` to `@/directory`, which means you now have to import like this:
|
||||||
|
|
||||||
|
```astro
|
||||||
|
---
|
||||||
|
import { slugifyStr } from "@/utils/slugify";
|
||||||
|
import IconHash from "@/assets/icons/IconHash.svg";
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
### Move to `pnpm`
|
||||||
|
|
||||||
|
AstroPaper has switched from `npm` to `pnpm`, which offers faster and more efficient package management.
|
||||||
|
|
||||||
|
### Replace icons/svg with Astro's Svg Component
|
||||||
|
|
||||||
|
AstroPaper v5 replaces inline SVGs with Astro’s experimental [SVG Component](https://docs.astro.build/en/reference/experimental-flags/svg/). This update reduces the need for predefined SVG code in the `socialIcons` object, making the codebase cleaner and more maintainable.
|
||||||
|
|
||||||
|
### Separate Constants and Config
|
||||||
|
|
||||||
|
The project structure has been reorganized. The `src/config.ts` file now only contains the `SITE` object, which holds the main configuration for the project. All constants, such as `LOCALE`, `SOCIALS`, and `SHARE_LINKS`, have been moved to the `src/constants.ts` file.
|
||||||
|
|
||||||
|
## Other notable changes
|
||||||
|
|
||||||
|
- The blog posts directory has been updated from `src/content/blog/` to `src/data/blog/`.
|
||||||
|
- Conllection definitions file (`src/content/config.ts`) is now replaced with `src/content.config.ts`.
|
||||||
|
- Various dependencies have been upgraded for improved performance and security.
|
||||||
|
- Removed `IBM Plex Mono` font and switched to the default system mono font.
|
||||||
|
- The `Go back` button logic has been updated. Now, instead of triggering the browser's history API, AstroPaper v5 uses the browser session to temporarily store the back URL. If no back URL exists in the session, it will redirect to the homepage.
|
||||||
|
- There are some minor styles and layout changes as well.
|
||||||
|
|
||||||
|
## Outtro
|
||||||
|
|
||||||
|
AstroPaper v5 brings many changes, but the core experience remains the same. Enjoy a smoother, more efficient blogging platform while keeping the clean and minimal design that AstroPaper is known for!
|
||||||
|
|
||||||
|
Feel free to explore the changes and share your thoughts. As always, thank you for your support!
|
||||||
|
|
||||||
|
If you enjoy this theme, please consider starring the repo. You can also support me via GitHub Sponsors or you can buy me a coffee if you'd like. However, of course, these actions are entirely optional and not required.
|
||||||
|
|
||||||
|
Enjoy!
|
||||||
|
|
||||||
|
[Sat Naing](https://satnaing.dev/)
|
208
src/data/blog/adding-new-post.md
Normal file
|
@ -0,0 +1,208 @@
|
||||||
|
---
|
||||||
|
author: Sat Naing
|
||||||
|
pubDatetime: 2022-09-23T15:22:00Z
|
||||||
|
modDatetime: 2025-03-22T06:25:46.734Z
|
||||||
|
title: Adding new posts in AstroPaper theme
|
||||||
|
slug: adding-new-posts-in-astropaper-theme
|
||||||
|
featured: true
|
||||||
|
draft: false
|
||||||
|
tags:
|
||||||
|
- docs
|
||||||
|
description:
|
||||||
|
Some rules & recommendations for creating or adding new posts using AstroPaper
|
||||||
|
theme.
|
||||||
|
---
|
||||||
|
|
||||||
|
Here are some rules/recommendations, tips & ticks for creating new posts in AstroPaper blog theme.
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img
|
||||||
|
src="https://images.pexels.com/photos/159618/still-life-school-retro-ink-159618.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1"
|
||||||
|
alt="Free Classic wooden desk with writing materials, vintage clock, and a leather bag. Stock Photo"
|
||||||
|
/>
|
||||||
|
<figcaption class="text-center">
|
||||||
|
Photo by <a href="https://www.pexels.com/photo/brown-wooden-desk-159618/">Pixabay</a>
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
## Table of contents
|
||||||
|
|
||||||
|
## Creating a Blog Post
|
||||||
|
|
||||||
|
To write a new blog post, create a markdown file inside the `src/data/blog/` directory.
|
||||||
|
|
||||||
|
> Prior to AstroPaper v5.1.0, all blog posts had to be in `src/data/blog/`, meaning you couldn't organize them into subdirectories.
|
||||||
|
|
||||||
|
Starting from AstroPaper v5.1.0, you can now organize blog posts into subdirectories, making it easier to manage your content.
|
||||||
|
|
||||||
|
For example, if you want to group posts under `2025`, you can place them in `src/data/blog/2025/`. This also affects the post URL, so `src/data/blog/2025/example-post.md` will be available at `/posts/2025/example-post`.
|
||||||
|
|
||||||
|
If you don’t want subdirectories to affect the post URL, just prefix the folder name with an underscore `_`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example: blog post structure and URLs
|
||||||
|
src/data/blog/very-first-post.md -> mysite.com/posts/very-first-post
|
||||||
|
src/data/blog/2025/example-post.md -> mysite.com/posts/2025/example-post
|
||||||
|
src/data/blog/_2026/another-post.md -> mysite.com/posts/another-post
|
||||||
|
src/data/blog/docs/_legacy/how-to.md -> mysite.com/docs/how-to
|
||||||
|
src/data/blog/Example Dir/Dummy Post.md -> mysite.com/example-dir/dummy-post
|
||||||
|
```
|
||||||
|
|
||||||
|
> 💡 Tip: You can override a blog post’s slug in the frontmatter as well. See the next section for more details.
|
||||||
|
|
||||||
|
If the subdirectory URL doesn’t appear in the build output, remove node_modules, reinstall packages, and then rebuild.
|
||||||
|
|
||||||
|
## Frontmatter
|
||||||
|
|
||||||
|
Frontmatter is the main place to store some important information about the blog post (article). Frontmatter lies at the top of the article and is written in YAML format. Read more about frontmatter and its usage in [astro documentation](https://docs.astro.build/en/guides/markdown-content/).
|
||||||
|
|
||||||
|
Here is the list of frontmatter property for each post.
|
||||||
|
|
||||||
|
| Property | Description | Remark |
|
||||||
|
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- |
|
||||||
|
| **_title_** | Title of the post. (h1) | required<sup>\*</sup> |
|
||||||
|
| **_description_** | Description of the post. Used in post excerpt and site description of the post. | required<sup>\*</sup> |
|
||||||
|
| **_pubDatetime_** | Published datetime in ISO 8601 format. | required<sup>\*</sup> |
|
||||||
|
| **_modDatetime_** | Modified datetime in ISO 8601 format. (only add this property when a blog post is modified) | optional |
|
||||||
|
| **_author_** | Author of the post. | default = SITE.author |
|
||||||
|
| **_slug_** | Slug for the post. This field is optional. | default = slugified file name |
|
||||||
|
| **_featured_** | Whether or not display this post in featured section of home page | default = false |
|
||||||
|
| **_draft_** | Mark this post 'unpublished'. | default = false |
|
||||||
|
| **_tags_** | Related keywords for this post. Written in array yaml format. | default = others |
|
||||||
|
| **_ogImage_** | OG image of the post. Useful for social media sharing and SEO. This can be a remote URL or an image path relative to current folder. | default = `SITE.ogImage` or generated OG image |
|
||||||
|
| **_canonicalURL_** | Canonical URL (absolute), in case the article already exists on other source. | default = `Astro.site` + `Astro.url.pathname` |
|
||||||
|
| **_hideEditPost_** | Hide editPost button under blog title. This applies only to the current blog post. | default = false |
|
||||||
|
| **_timezone_** | Specify a timezone in IANA format for the current blog post. This will override the `SITE.timezone` config for the current blog post. | default = `SITE.timezone` |
|
||||||
|
|
||||||
|
> Tip! You can get ISO 8601 datetime by running `new Date().toISOString()` in the console. Make sure you remove quotes though.
|
||||||
|
|
||||||
|
Only `title`, `description` and `pubDatetime` fields in frontmatter must be specified.
|
||||||
|
|
||||||
|
Title and description (excerpt) are important for search engine optimization (SEO) and thus AstroPaper encourages to include these in blog posts.
|
||||||
|
|
||||||
|
`slug` is the unique identifier of the url. Thus, `slug` must be unique and different from other posts. The whitespace of `slug` should to be separated with `-` or `_` but `-` is recommended. Slug is automatically generated using the blog post file name. However, you can define your `slug` as a frontmatter in your blog post.
|
||||||
|
|
||||||
|
For example, if the blog file name is `adding-new-post.md` and you don't specify the slug in your frontmatter, Astro will automatically create a slug for the blog post using the file name. Thus, the slug will be `adding-new-post`. But if you specify the `slug` in the frontmatter, this will override the default slug. You can read more about this in [Astro Docs](https://docs.astro.build/en/guides/content-collections/#defining-custom-slugs).
|
||||||
|
|
||||||
|
If you omit `tags` in a blog post (in other words, if no tag is specified), the default tag `others` will be used as a tag for that post. You can set the default tag in the `/src/content/config.ts` file.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/content/config.ts
|
||||||
|
export const blogSchema = z.object({
|
||||||
|
// ---
|
||||||
|
draft: z.boolean().optional(),
|
||||||
|
tags: z.array(z.string()).default(["others"]), // replace "others" with whatever you want
|
||||||
|
// ---
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample Frontmatter
|
||||||
|
|
||||||
|
Here is the sample frontmatter for a post.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# src/content/blog/sample-post.md
|
||||||
|
---
|
||||||
|
title: The title of the post
|
||||||
|
author: your name
|
||||||
|
pubDatetime: 2022-09-21T05:17:19Z
|
||||||
|
slug: the-title-of-the-post
|
||||||
|
featured: true
|
||||||
|
draft: false
|
||||||
|
tags:
|
||||||
|
- some
|
||||||
|
- example
|
||||||
|
- tags
|
||||||
|
ogImage: ../../assets/images/example.png # src/assets/images/example.png
|
||||||
|
# ogImage: "https://example.org/remote-image.png" # remote URL
|
||||||
|
description: This is the example description of the example post.
|
||||||
|
canonicalURL: https://example.org/my-article-was-already-posted-here
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding table of contents
|
||||||
|
|
||||||
|
By default, a post (article) does not include any table of contents (toc). To include toc, you have to specify it in a specific way.
|
||||||
|
|
||||||
|
Write `Table of contents` in h2 format (## in markdown) and place it where you want it to be appeared on the post.
|
||||||
|
|
||||||
|
For instance, if you want to place your table of contents just under the intro paragraph (like I usually do), you can do that in the following way.
|
||||||
|
|
||||||
|
```md
|
||||||
|
---
|
||||||
|
# some frontmatter
|
||||||
|
---
|
||||||
|
|
||||||
|
Here are some recommendations, tips & ticks for creating new posts in AstroPaper blog theme.
|
||||||
|
|
||||||
|
## Table of contents
|
||||||
|
|
||||||
|
<!-- the rest of the post -->
|
||||||
|
```
|
||||||
|
|
||||||
|
## Headings
|
||||||
|
|
||||||
|
There's one thing to note about headings. The AstroPaper blog posts use title (title in the frontmatter) as the main heading of the post. Therefore, the rest of the heading in the post should be using h2 \~ h6.
|
||||||
|
|
||||||
|
This rule is not mandatory, but highly recommended for visual, accessibility and SEO purposes.
|
||||||
|
|
||||||
|
## Storing Images for Blog Content
|
||||||
|
|
||||||
|
Here are two methods for storing images and displaying them inside a markdown file.
|
||||||
|
|
||||||
|
> Note! If it's a requirement to style optimized images in markdown you should [use MDX](https://docs.astro.build/en/guides/images/#images-in-mdx-files).
|
||||||
|
|
||||||
|
### Inside `src/assets/` directory (recommended)
|
||||||
|
|
||||||
|
You can store images inside `src/assets/` directory. These images will be automatically optimized by Astro through [Image Service API](https://docs.astro.build/en/reference/image-service-reference/).
|
||||||
|
|
||||||
|
You can use relative path or alias path (`@/assets/`) to serve these images.
|
||||||
|
|
||||||
|
Example: Suppose you want to display `example.jpg` whose path is `/src/assets/images/example.jpg`.
|
||||||
|
|
||||||
|
```md
|
||||||
|

|
||||||
|
|
||||||
|
<!-- OR -->
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<!-- Using img tag or Image component won't work ❌ -->
|
||||||
|
<img src="@/assets/images/example.jpg" alt="something">
|
||||||
|
<!-- ^^ This is wrong -->
|
||||||
|
```
|
||||||
|
|
||||||
|
> Technically, you can store images inside any directory under `src`. In here, `src/assets` is just a recommendation.
|
||||||
|
|
||||||
|
### Inside `public` directory
|
||||||
|
|
||||||
|
You can store images inside the `public` directory. Keep in mind that images stored in the `public` directory remain untouched by Astro, meaning they will be unoptimized and you need to handle image optimization by yourself.
|
||||||
|
|
||||||
|
For these images, you should use an absolute path; and these images can be displayed using [markdown annotation](https://www.markdownguide.org/basic-syntax/#images-1) or [HTML img tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img).
|
||||||
|
|
||||||
|
Example: Assume `example.jpg` is located at `/public/assets/images/example.jpg`.
|
||||||
|
|
||||||
|
```md
|
||||||
|

|
||||||
|
|
||||||
|
<!-- OR -->
|
||||||
|
|
||||||
|
<img src="/assets/images/example.jpg" alt="something">
|
||||||
|
```
|
||||||
|
|
||||||
|
## Bonus
|
||||||
|
|
||||||
|
### Image compression
|
||||||
|
|
||||||
|
When you put images in the blog post (especially for images under `public` directory), it is recommended that the image is compressed. This will affect the overall performance of the website.
|
||||||
|
|
||||||
|
My recommendation for image compression sites.
|
||||||
|
|
||||||
|
- [TinyPng](https://tinypng.com/)
|
||||||
|
- [TinyJPG](https://tinyjpg.com/)
|
||||||
|
|
||||||
|
### OG Image
|
||||||
|
|
||||||
|
The default OG image will be placed if a post does not specify the OG image. Though not required, OG image related to the post should be specify in the frontmatter. The recommended size for OG image is **_1200 X 640_** px.
|
||||||
|
|
||||||
|
> Since AstroPaper v1.4.0, OG images will be generated automatically if not specified. Check out [the announcement](https://astro-paper.pages.dev/posts/dynamic-og-image-generation-in-astropaper-blog-posts/).
|
132
src/data/blog/customizing-astropaper-theme-color-schemes.md
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
---
|
||||||
|
author: Sat Naing
|
||||||
|
pubDatetime: 2022-09-25T15:20:35Z
|
||||||
|
title: Customizing AstroPaper theme color schemes
|
||||||
|
featured: false
|
||||||
|
draft: false
|
||||||
|
tags:
|
||||||
|
- color-schemes
|
||||||
|
- docs
|
||||||
|
description:
|
||||||
|
How you can enable/disable light & dark mode; and customize color schemes
|
||||||
|
of AstroPaper theme.
|
||||||
|
---
|
||||||
|
|
||||||
|
This post will explain how you can enable/disable light & dark mode for the website. Moreover, you'll learn how you can customize color schemes of the entire website.
|
||||||
|
|
||||||
|
## Table of contents
|
||||||
|
|
||||||
|
## Enable/disable light & dark mode
|
||||||
|
|
||||||
|
AstroPaper theme will include light and dark mode by default. In other words, there will be two color schemes\_ one for light mode and another for dark mode. This default behavior can be disabled in SITE configuration object of the `src/config.ts` file.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// file: src/config.ts
|
||||||
|
export const SITE = {
|
||||||
|
website: "https://astro-paper.pages.dev/",
|
||||||
|
author: "Sat Naing",
|
||||||
|
desc: "A minimal, responsive and SEO-friendly Astro blog theme.",
|
||||||
|
title: "AstroPaper",
|
||||||
|
ogImage: "astropaper-og.jpg",
|
||||||
|
lightAndDarkMode: true, // true by default
|
||||||
|
postPerPage: 3,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
To disable `light & dark mode` set `SITE.lightAndDarkMode` to `false`.
|
||||||
|
|
||||||
|
## Choose primary color scheme
|
||||||
|
|
||||||
|
By default, if we disable `SITE.lightAndDarkMode`, we will only get system's prefers-color-scheme.
|
||||||
|
|
||||||
|
Thus, to choose primary color scheme instead of prefers-color-scheme, we have to set color scheme in the primaryColorScheme variable inside `public/toggle-theme.js`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
/* file: public/toggle-theme.js */
|
||||||
|
const primaryColorScheme = ""; // "light" | "dark"
|
||||||
|
|
||||||
|
// Get theme data from local storage
|
||||||
|
const currentTheme = localStorage.getItem("theme");
|
||||||
|
|
||||||
|
// other codes etc...
|
||||||
|
```
|
||||||
|
|
||||||
|
The **primaryColorScheme** variable can hold two values\_ `"light"`, `"dark"`. You can leave the empty string (default) if you don't want to specify the primary color scheme.
|
||||||
|
|
||||||
|
- `""` - system's prefers-color-scheme. (default)
|
||||||
|
- `"light"` - use light mode as primary color scheme.
|
||||||
|
- `"dark"` - use dark mode as primary color scheme.
|
||||||
|
|
||||||
|
<details><summary>Why 'primaryColorScheme' is not inside config.ts?</summary>
|
||||||
|
|
||||||
|
> To avoid color flickering on page reload, we have to place the toggle-switch JavaScript codes as early as possible when the page loads. It solves the problem of flickering, but as a trade-off, we cannot use ESM imports anymore.
|
||||||
|
|
||||||
|
[Click here](https://docs.astro.build/en/reference/directives-reference/#isinline) to know more about Astro's `is:inline` script.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## Customize color schemes
|
||||||
|
|
||||||
|
Both light & dark color schemes of AstroPaper theme can be customized. You can do this in `src/styles/base.css` file.
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* file: src/styles/base.css */
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root,
|
||||||
|
html[data-theme="light"] {
|
||||||
|
--color-fill: 251, 254, 251;
|
||||||
|
--color-text-base: 40, 39, 40;
|
||||||
|
--color-accent: 0, 108, 172;
|
||||||
|
--color-card: 230, 230, 230;
|
||||||
|
--color-card-muted: 205, 205, 205;
|
||||||
|
--color-border: 236, 233, 233;
|
||||||
|
}
|
||||||
|
html[data-theme="dark"] {
|
||||||
|
--color-fill: 47, 55, 65;
|
||||||
|
--color-text-base: 230, 230, 230;
|
||||||
|
--color-accent: 26, 217, 217;
|
||||||
|
--color-card: 63, 75, 90;
|
||||||
|
--color-card-muted: 89, 107, 129;
|
||||||
|
--color-border: 59, 70, 85;
|
||||||
|
}
|
||||||
|
/* other styles */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In AstroPaper theme, `:root` and `html[data-theme="light"]` selectors are used as the light color scheme and `html[data-theme="dark"]` is used the dark color scheme. If you want to customize your custom color scheme, you have to specify your light color scheme inside `:root`,`html[data-theme="light"]` and dark color scheme inside `html[data-theme="dark"]`.
|
||||||
|
|
||||||
|
Colors are declared in CSS custom property (CSS Variable) notation. Color property values are written in rgb values. (Note: instead of `rgb(40, 39, 40)`, only specify `40, 39, 40`)
|
||||||
|
|
||||||
|
Here is the detail explanation of color properties.
|
||||||
|
|
||||||
|
| Color Property | Definition & Usage |
|
||||||
|
| -------------------- | ---------------------------------------------------------- |
|
||||||
|
| `--color-fill` | Primary color of the website. Usually the main background. |
|
||||||
|
| `--color-text-base` | Secondary color of the website. Usually the text color. |
|
||||||
|
| `--color-accent` | Accent color of the website. Link color, hover color etc. |
|
||||||
|
| `--color-card` | Card, scrollbar and code background color (like `this`). |
|
||||||
|
| `--color-card-muted` | Card and scrollbar background color for hover state etc. |
|
||||||
|
| `--color-border` | Border color. Especially used in horizontal row (hr) |
|
||||||
|
|
||||||
|
Here is an example of changing the light color scheme.
|
||||||
|
|
||||||
|
```css
|
||||||
|
@layer base {
|
||||||
|
/* lobster color scheme */
|
||||||
|
:root,
|
||||||
|
html[data-theme="light"] {
|
||||||
|
--color-fill: 246, 238, 225;
|
||||||
|
--color-text-base: 1, 44, 86;
|
||||||
|
--color-accent: 225, 74, 57;
|
||||||
|
--color-card: 220, 152, 145;
|
||||||
|
--color-card-muted: 233, 119, 106;
|
||||||
|
--color-border: 220, 152, 145;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> Check out some [predefined color schemes](https://astro-paper.pages.dev/posts/predefined-color-schemes/) AstroPaper has already crafted for you.
|
97
src/data/blog/dynamic-og-images.md
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
---
|
||||||
|
author: Sat Naing
|
||||||
|
pubDatetime: 2022-12-28T04:59:04.866Z
|
||||||
|
modDatetime: 2025-03-12T13:39:20.763Z
|
||||||
|
title: Dynamic OG image generation in AstroPaper blog posts
|
||||||
|
slug: dynamic-og-image-generation-in-astropaper-blog-posts
|
||||||
|
featured: false
|
||||||
|
draft: false
|
||||||
|
tags:
|
||||||
|
- docs
|
||||||
|
- release
|
||||||
|
description: New feature in AstroPaper v1.4.0, introducing dynamic OG image generation for blog posts.
|
||||||
|
---
|
||||||
|
|
||||||
|
New feature in AstroPaper v1.4.0, introducing dynamic OG image generation for blog posts.
|
||||||
|
|
||||||
|
## Table of contents
|
||||||
|
|
||||||
|
## Intro
|
||||||
|
|
||||||
|
OG images (aka Social Images) play an important role in social media engagements. In case you don't know what OG image means, it is an image displayed whenever we share our website URL on social media such as Facebook, Discord etc.
|
||||||
|
|
||||||
|
> The Social Image used for Twitter is technically not called OG image. However, in this post, I'll be using the term OG image for all types of Social Images.
|
||||||
|
|
||||||
|
## Default/Static OG image (the old way)
|
||||||
|
|
||||||
|
AstroPaper already provided a way to add an OG image to a blog post. The author can specify the OG image in the frontmatter `ogImage`. Even when the author doesn't define the OG image in the frontmatter, the default OG image will be used as a fallback (in this case `public/astropaper-og.jpg`). But the problem is that the default OG image is static, which means every blog post that does not include an OG image in the frontmatter will always use the same default OG image despite each post title/content being different from others.
|
||||||
|
|
||||||
|
## Dynamic OG Image
|
||||||
|
|
||||||
|
Generating a dynamic OG image for each post allows the author to avoid specifying an OG image for every single blog post. Besides, this will prevent the fallback OG image from being identical to all blog posts.
|
||||||
|
|
||||||
|
In AstroPaper v1.4.0, Vercel's [Satori](https://github.com/vercel/satori) package is used for dynamic OG image generation.
|
||||||
|
|
||||||
|
Dynamic OG images will be generated at build time for blog posts that
|
||||||
|
|
||||||
|
- don't include OG image in the frontmatter
|
||||||
|
- are not marked as draft.
|
||||||
|
|
||||||
|
## Anatomy of AstroPaper dynamic OG image
|
||||||
|
|
||||||
|
Dynamic OG image of AstroPaper includes _the blog post title_, _author name_ and _site title_. Author name and site title will be retrieved via `SITE.author` and `SITE.title` of **"src/config.ts"** file. The title is generated from the blog post frontmatter `title`.
|
||||||
|

|
||||||
|
|
||||||
|
### Issue Non-Latin Characters
|
||||||
|
|
||||||
|
Titles with non-latin characters won't display properly out of the box. To resolve this, we have to replace `fontsConfig` inside `loadGoogleFont.ts` with your preferred font.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// file: loadGoogleFont.ts
|
||||||
|
|
||||||
|
async function loadGoogleFonts(
|
||||||
|
text: string
|
||||||
|
): Promise<
|
||||||
|
Array<{ name: string; data: ArrayBuffer; weight: number; style: string }>
|
||||||
|
> {
|
||||||
|
const fontsConfig = [
|
||||||
|
{
|
||||||
|
name: "Noto Sans JP",
|
||||||
|
font: "Noto+Sans+JP",
|
||||||
|
weight: 400,
|
||||||
|
style: "normal",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Noto Sans JP",
|
||||||
|
font: "Noto+Sans+JP:wght@700",
|
||||||
|
weight: 700,
|
||||||
|
style: "normal",
|
||||||
|
},
|
||||||
|
{ name: "Noto Sans", font: "Noto+Sans", weight: 400, style: "normal" },
|
||||||
|
{
|
||||||
|
name: "Noto Sans",
|
||||||
|
font: "Noto+Sans:wght@700",
|
||||||
|
weight: 700,
|
||||||
|
style: "normal",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
// other codes
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> Check out [this PR](https://github.com/satnaing/astro-paper/pull/318) for more info.
|
||||||
|
|
||||||
|
## Trade-off
|
||||||
|
|
||||||
|
While this is a nice feature to have, there's a trade-off. Each OG image takes roughly one second to generate. This might not be noticeable at first, but as the number of blog posts grows, you might want to disable this feature. Since every OG image takes time to generate, having many of them will increase the build time linearly.
|
||||||
|
|
||||||
|
For example: If one OG image takes one second to generate, then 60 images will take around one minute, and 600 images will take approximately 10 minutes. This can significantly impact build times as your content scales.
|
||||||
|
|
||||||
|
Related issue: [#428](https://github.com/satnaing/astro-paper/issues/428)
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
At the time of writing this, [Satori](https://github.com/vercel/satori) is fairly new and has not reached major release yet. So, there are still some limitations to this dynamic OG image feature.
|
||||||
|
|
||||||
|
- Besides, RTL languages are not supported yet.
|
||||||
|
- [Using emoji](https://github.com/vercel/satori#emojis) in the title might be a little bit tricky.
|
21
src/data/blog/examples/example-draft-post.md
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
---
|
||||||
|
title: Example Draft Post
|
||||||
|
author: Sat Naing
|
||||||
|
pubDatetime: 2022-06-06T04:06:31Z
|
||||||
|
slug: example-draft-post
|
||||||
|
featured: false
|
||||||
|
draft: true
|
||||||
|
tags:
|
||||||
|
- TypeScript
|
||||||
|
- Astro
|
||||||
|
description:
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
|
||||||
|
incididunt ut labore et dolore magna aliqua. Praesent elementum facilisis leo vel
|
||||||
|
fringilla est
|
||||||
|
---
|
||||||
|
|
||||||
|
Users cannot see this post because it is in draft.
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
rec 1
|
105
src/data/blog/examples/portfolio-website-development.md
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
---
|
||||||
|
title: How Do I Develop My Portfolio Website & Blog
|
||||||
|
author: Sat Naing
|
||||||
|
pubDatetime: 2022-03-25T16:55:12.000+00:00
|
||||||
|
slug: how-do-i-develop-my-portfolio-and-blog
|
||||||
|
featured: false
|
||||||
|
draft: false
|
||||||
|
tags:
|
||||||
|
- NextJS
|
||||||
|
- TailwindCSS
|
||||||
|
- HeadlessCMS
|
||||||
|
- Blog
|
||||||
|
description:
|
||||||
|
"EXAMPLE POST: My experience about developing my first portfolio website and a blog
|
||||||
|
using NextJS and a headless CMS."
|
||||||
|
timezone: "Asia/Yangon"
|
||||||
|
---
|
||||||
|
|
||||||
|
> This article is originally from my [blog post](https://satnaing.dev/blog/posts/how-do-i-develop-my-portfolio-and-blog). I put this article to demonstrate how you can write blog posts/articles using AstroPaper theme.
|
||||||
|
|
||||||
|
My experience about developing my first portfolio website and a blog using NextJS and a headless CMS.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
I've been always thinking about launching my own website with my custom domain name (**satnaing.dev**) since my college student life. But that never happened until this project. I've done several projects and works about web application development but I didn't make an effort to do this.
|
||||||
|
|
||||||
|
So, "what about blog?" you may ask. Yeah, blog also has been in my project list for some time. I always wanted to make a blog project using some of the latest technologies. However, I've been busy with my works and other projects so that blog project has never been started.
|
||||||
|
|
||||||
|
In these days, I tend to develop my own projects with the focus in good quality rather than quantity. After the project is done, I usually put a proper readme file in the Github repo. But Github repo readme is only suitable for technical aspects (this is just my thought). I want to write down my experiences and challenges. Thus, I decided to make my own blog. Plus, at this point, I have decent experiences and confidence to develop this project.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
For the front-end, I wanted to use [React](https://reactjs.org/ "React Official Website"). But React alone is not good enough for SEO; and I did have to consider many factors like routing, image optimization etc. So, I chose [NextJS](https://nextjs.org/ "NextJS Official Website") as my main front-end stack. And of course TypeScript for type checking. (It's said that you'll love TypeScript when you're used to it 😉)
|
||||||
|
|
||||||
|
For styling, I use [TailwindCSS](https://tailwindcss.com/ "Tailwind CSS Official Website"). This is because I love developer experience that Tailwind gives and it has a lot of flexibilities compared to other component UI libraries like MUI or React Bootstrap.
|
||||||
|
|
||||||
|
All contents of this project reside within the GitHub repository. All my blog posts (including this one) are written in Markdown file format since I'm very used to with this. But to write Markdown along with its frontmatter effortlessly, I use [Forestry](https://forestry.io/ "Forestry Official Website") headless CMS. It is a git-based CMS that can serve Markdown and other contents. Because of this, I can write my contents either using Markdown or wysiwyg editor. Besides, writing frontmatters with this is a breeze.
|
||||||
|
|
||||||
|
Images and assets are uploaded and stored in [Cloudinary](https://cloudinary.com/ "Cloudinary Official Website"). I connect Cloudinary via Forestry and manage them directly in the dashboard.
|
||||||
|
|
||||||
|
In conclusion, these are the tech stack I've used for this project.
|
||||||
|
|
||||||
|
- Front-end: NextJS (TypeScript)
|
||||||
|
- Styling: TailwindCSS
|
||||||
|
- Animations: GSAP
|
||||||
|
- CMS: Forestry Headless CMS
|
||||||
|
- Deployment: Vercel
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
The following are certain features of my portfolio and blog
|
||||||
|
|
||||||
|
### SEO Friendly
|
||||||
|
|
||||||
|
The entire project is developed with SEO focus in mind. I've used proper meta tags, descriptions and heading alignments. This website is now indexed by Google.
|
||||||
|
|
||||||
|
> You can search this website on google by using keywords like 'sat naing dev'
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Moreover, this website will be displayed well when shared to social media due to properly used meta tags.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Dynamic Sitemap
|
||||||
|
|
||||||
|
Sitemap plays an important part in SEO. Because of this, every single page of this site should be included in sitemap.xml. I made an auto generated sitemap in my website whenever I create a new content or tags or categories.
|
||||||
|
|
||||||
|
### Light & Dark Themes
|
||||||
|
|
||||||
|
Due to dark theme trend in recent years, many websites include dark theme out of the box nowadays. Certainly, my website also supports light & dark themes.
|
||||||
|
|
||||||
|
### Fully Accessible
|
||||||
|
|
||||||
|
This website is fully accessible. You can navigate around by only using keyboard. I put all a11y enhancement best practices like including alt text in all images, no skipping headings, using semantic HTML tags, using aria-attributes properly.
|
||||||
|
|
||||||
|
### Search box, Categories & Tags
|
||||||
|
|
||||||
|
All blog contents can be searched by search box. Moreover, contents can be filtered by categories and tags. In this way, blog readers can search and read what they really want.
|
||||||
|
|
||||||
|
### Performance and Lighthouse Score
|
||||||
|
|
||||||
|
This website got very good performance and lighthouse score thanks to proper development and best practices. Here's the lighthouse score for this website.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Animations
|
||||||
|
|
||||||
|
Initially I used [Framer Motion](https://www.framer.com/motion/ "Framer Motion") to add animations and micro interactions for this website. However, when I tried to use some complex animations and parallax effects, I found it inconvenient to integrate with Framer Motion (Maybe I'm not very good at and used to working with it). Hence, I decided to use [GSAP](https://greensock.com/ "GSAP Animation Library") for all of my animations. It is one of the most popular animation library and it is capable of doing complex and advanced animations. You can see animations and micro interactions on pretty much every page of this website.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Outro
|
||||||
|
|
||||||
|
In conclusion, this project gives me a lot of experience and confidence about developing blog site (SSG). Now, I have gained knowledge of git-based CMS and how it interacts with NextJS. I've also learned about SEO, dynamic sitemap generation and indexing Google procedures. I will make better projects in the future. So, stay tuned! ✌🏻
|
||||||
|
|
||||||
|
And... last but not least, I would like to say 'thanks' to my friend [Swann Fevian Kyaw](https://www.facebook.com/bon.zai.3910 "Swann Fevian Kyaw's Facebook Account") (@[ToonHa](https://www.facebook.com/ToonHa-102639465752883 "ToonHa Facebook Page")) who has drawn a beautiful illustration for my hero section of the website.
|
||||||
|
|
||||||
|
## Project Links
|
||||||
|
|
||||||
|
- Website: [https://satnaing.dev/](https://satnaing.dev/ "https://satnaing.dev/")
|
||||||
|
- Blog: [https://satnaing.dev/blog](https://satnaing.dev/blog "https://satnaing.dev/blog")
|
||||||
|
- Repo: [https://github.com/satnaing/my-portfolio](https://github.com/satnaing/my-portfolio "https://github.com/satnaing/my-portfolio")
|
208
src/data/blog/examples/tailwind-typography.md
Normal file
|
@ -0,0 +1,208 @@
|
||||||
|
---
|
||||||
|
title: Tailwind Typography Plugin
|
||||||
|
author: Sat Naing
|
||||||
|
pubDatetime: 2022-07-05T02:05:51Z
|
||||||
|
featured: false
|
||||||
|
draft: false
|
||||||
|
tags:
|
||||||
|
- TypeScript
|
||||||
|
- Astro
|
||||||
|
description: "EXAMPLE POST: About Tailwind Typography Plugin and how you can use it effectively."
|
||||||
|
---
|
||||||
|
|
||||||
|
> This article is from [TailwindLabs](https://tailwindcss-typography.vercel.app/). I put this article to demonstrate how you can write blog posts/articles using AstroPaper theme.
|
||||||
|
|
||||||
|
By default, Tailwind removes all of the default browser styling from paragraphs, headings, lists and more. This ends up being really useful for building application UIs because you spend less time undoing user-agent styles, but when you _really are_ just trying to style some content that came from a rich-text editor in a CMS or a markdown file, it can be surprising and unintuitive.
|
||||||
|
|
||||||
|
We get lots of complaints about it actually, with people regularly asking us things like:
|
||||||
|
|
||||||
|
> Why is Tailwind removing the default styles on my `h1` elements? How do I disable this? What do you mean I lose all the other base styles too?
|
||||||
|
> We hear you, but we're not convinced that simply disabling our base styles is what you really want. You don't want to have to remove annoying margins every time you use a `p` element in a piece of your dashboard UI. And I doubt you really want your blog posts to use the user-agent styles either — you want them to look _awesome_, not awful.
|
||||||
|
|
||||||
|
The `@tailwindcss/typography` plugin is our attempt to give you what you _actually_ want, without any of the downsides of doing something stupid like disabling our base styles.
|
||||||
|
|
||||||
|
It adds a new `prose` class that you can slap on any block of vanilla HTML content and turn it into a beautiful, well-formatted document:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<article class="prose">
|
||||||
|
<h1>Garlic bread with cheese: What the science tells us</h1>
|
||||||
|
<p>
|
||||||
|
For years parents have espoused the health benefits of eating garlic bread
|
||||||
|
with cheese to their children, with the food earning such an iconic status
|
||||||
|
in our culture that kids will often dress up as warm, cheesy loaf for
|
||||||
|
Halloween.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
But a recent study shows that the celebrated appetizer may be linked to a
|
||||||
|
series of rabies cases springing up around the country.
|
||||||
|
</p>
|
||||||
|
<!-- ... -->
|
||||||
|
</article>
|
||||||
|
```
|
||||||
|
|
||||||
|
For more information about how to use the plugin and the features it includes, [read the documentation](https://github.com/tailwindcss/typography/blob/master/README.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What to expect from here on out
|
||||||
|
|
||||||
|
What follows from here is just a bunch of absolute nonsense I've written to dogfood the plugin itself. It includes every sensible typographic element I could think of, like **bold text**, unordered lists, ordered lists, code blocks, block quotes, _and even italics_.
|
||||||
|
|
||||||
|
It's important to cover all of these use cases for a few reasons:
|
||||||
|
|
||||||
|
1. We want everything to look good out of the box.
|
||||||
|
2. Really just the first reason, that's the whole point of the plugin.
|
||||||
|
3. Here's a third pretend reason though a list with three items looks more realistic than a list with two items.
|
||||||
|
|
||||||
|
Now we're going to try out another header style.
|
||||||
|
|
||||||
|
### Typography should be easy
|
||||||
|
|
||||||
|
So that's a header for you — with any luck if we've done our job correctly that will look pretty reasonable.
|
||||||
|
|
||||||
|
Something a wise person once told me about typography is:
|
||||||
|
|
||||||
|
> Typography is pretty important if you don't want your stuff to look like trash. Make it good then it won't be bad.
|
||||||
|
> It's probably important that images look okay here by default as well:
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img
|
||||||
|
src="https://images.unsplash.com/photo-1556740758-90de374c12ad?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1000&q=80"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<figcaption>
|
||||||
|
Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of
|
||||||
|
classical Latin literature from 45 BC, making it over 2000 years old.
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
Now I'm going to show you an example of an unordered list to make sure that looks good, too:
|
||||||
|
|
||||||
|
- So here is the first item in this list.
|
||||||
|
- In this example we're keeping the items short.
|
||||||
|
- Later, we'll use longer, more complex list items.
|
||||||
|
|
||||||
|
And that's the end of this section.
|
||||||
|
|
||||||
|
## What if we stack headings?
|
||||||
|
|
||||||
|
### We should make sure that looks good, too.
|
||||||
|
|
||||||
|
Sometimes you have headings directly underneath each other. In those cases you often have to undo the top margin on the second heading because it usually looks better for the headings to be closer together than a paragraph followed by a heading should be.
|
||||||
|
|
||||||
|
### When a heading comes after a paragraph …
|
||||||
|
|
||||||
|
When a heading comes after a paragraph, we need a bit more space, like I already mentioned above. Now let's see what a more complex list would look like.
|
||||||
|
|
||||||
|
- **I often do this thing where list items have headings.**
|
||||||
|
|
||||||
|
For some reason I think this looks cool which is unfortunate because it's pretty annoying to get the styles right.
|
||||||
|
|
||||||
|
I often have two or three paragraphs in these list items, too, so the hard part is getting the spacing between the paragraphs, list item heading, and separate list items to all make sense. Pretty tough honestly, you could make a strong argument that you just shouldn't write this way.
|
||||||
|
|
||||||
|
- **Since this is a list, I need at least two items.**
|
||||||
|
|
||||||
|
I explained what I'm doing already in the previous list item, but a list wouldn't be a list if it only had one item, and we really want this to look realistic. That's why I've added this second list item so I actually have something to look at when writing the styles.
|
||||||
|
|
||||||
|
- **It's not a bad idea to add a third item either.**
|
||||||
|
|
||||||
|
I think it probably would've been fine to just use two items but three is definitely not worse, and since I seem to be having no trouble making up arbitrary things to type, I might as well include it.
|
||||||
|
|
||||||
|
After this sort of list I usually have a closing statement or paragraph, because it kinda looks weird jumping right to a heading.
|
||||||
|
|
||||||
|
## Code should look okay by default.
|
||||||
|
|
||||||
|
I think most people are going to use [highlight.js](https://highlightjs.org/) or [Prism](https://prismjs.com/) or something if they want to style their code blocks but it wouldn't hurt to make them look _okay_ out of the box, even with no syntax highlighting.
|
||||||
|
|
||||||
|
Here's what a default `tailwind.config.js` file looks like at the time of writing:
|
||||||
|
|
||||||
|
```js
|
||||||
|
module.exports = {
|
||||||
|
purge: [],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
variants: {},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Hopefully that looks good enough to you.
|
||||||
|
|
||||||
|
### What about nested lists?
|
||||||
|
|
||||||
|
Nested lists basically always look bad which is why editors like Medium don't even let you do it, but I guess since some of you goofballs are going to do it we have to carry the burden of at least making it work.
|
||||||
|
|
||||||
|
1. **Nested lists are rarely a good idea.**
|
||||||
|
- You might feel like you are being really "organized" or something but you are just creating a gross shape on the screen that is hard to read.
|
||||||
|
- Nested navigation in UIs is a bad idea too, keep things as flat as possible.
|
||||||
|
- Nesting tons of folders in your source code is also not helpful.
|
||||||
|
2. **Since we need to have more items, here's another one.**
|
||||||
|
- I'm not sure if we'll bother styling more than two levels deep.
|
||||||
|
- Two is already too much, three is guaranteed to be a bad idea.
|
||||||
|
- If you nest four levels deep you belong in prison.
|
||||||
|
3. **Two items isn't really a list, three is good though.**
|
||||||
|
- Again please don't nest lists if you want people to actually read your content.
|
||||||
|
- Nobody wants to look at this.
|
||||||
|
- I'm upset that we even have to bother styling this.
|
||||||
|
|
||||||
|
The most annoying thing about lists in Markdown is that `<li>` elements aren't given a child `<p>` tag unless there are multiple paragraphs in the list item. That means I have to worry about styling that annoying situation too.
|
||||||
|
|
||||||
|
- **For example, here's another nested list.**
|
||||||
|
|
||||||
|
But this time with a second paragraph.
|
||||||
|
|
||||||
|
- These list items won't have `<p>` tags
|
||||||
|
- Because they are only one line each
|
||||||
|
|
||||||
|
- **But in this second top-level list item, they will.**
|
||||||
|
|
||||||
|
This is especially annoying because of the spacing on this paragraph.
|
||||||
|
|
||||||
|
- As you can see here, because I've added a second line, this list item now has a `<p>` tag.
|
||||||
|
|
||||||
|
This is the second line I'm talking about by the way.
|
||||||
|
|
||||||
|
- Finally here's another list item so it's more like a list.
|
||||||
|
|
||||||
|
- A closing list item, but with no nested list, because why not?
|
||||||
|
|
||||||
|
And finally a sentence to close off this section.
|
||||||
|
|
||||||
|
## There are other elements we need to style
|
||||||
|
|
||||||
|
I almost forgot to mention links, like [this link to the Tailwind CSS website](https://tailwindcss.com). We almost made them blue but that's so yesterday, so we went with dark gray, feels edgier.
|
||||||
|
|
||||||
|
We even included table styles, check it out:
|
||||||
|
|
||||||
|
| Wrestler | Origin | Finisher |
|
||||||
|
| ----------------------- | ------------ | ------------------ |
|
||||||
|
| Bret "The Hitman" Hart | Calgary, AB | Sharpshooter |
|
||||||
|
| Stone Cold Steve Austin | Austin, TX | Stone Cold Stunner |
|
||||||
|
| Randy Savage | Sarasota, FL | Elbow Drop |
|
||||||
|
| Vader | Boulder, CO | Vader Bomb |
|
||||||
|
| Razor Ramon | Chuluota, FL | Razor's Edge |
|
||||||
|
|
||||||
|
We also need to make sure inline code looks good, like if I wanted to talk about `<span>` elements or tell you the good news about `@tailwindcss/typography`.
|
||||||
|
|
||||||
|
### Sometimes I even use `code` in headings
|
||||||
|
|
||||||
|
Even though it's probably a bad idea, and historically I've had a hard time making it look good. This _"wrap the code blocks in backticks"_ trick works pretty well though really.
|
||||||
|
|
||||||
|
Another thing I've done in the past is put a `code` tag inside of a link, like if I wanted to tell you about the [`tailwindcss/docs`](https://github.com/tailwindcss/docs) repository. I don't love that there is an underline below the backticks but it is absolutely not worth the madness it would require to avoid it.
|
||||||
|
|
||||||
|
#### We haven't used an `h4` yet
|
||||||
|
|
||||||
|
But now we have. Please don't use `h5` or `h6` in your content, Medium only supports two heading levels for a reason, you animals. I honestly considered using a `before` pseudo-element to scream at you if you use an `h5` or `h6`.
|
||||||
|
|
||||||
|
We don't style them at all out of the box because `h4` elements are already so small that they are the same size as the body copy. What are we supposed to do with an `h5`, make it _smaller_ than the body copy? No thanks.
|
||||||
|
|
||||||
|
### We still need to think about stacked headings though.
|
||||||
|
|
||||||
|
#### Let's make sure we don't screw that up with `h4` elements, either.
|
||||||
|
|
||||||
|
Phew, with any luck we have styled the headings above this text and they look pretty good.
|
||||||
|
|
||||||
|
Let's add a closing paragraph here so things end with a decently sized block of text. I can't explain why I want things to end that way but I have to assume it's because I think things will look weird or unbalanced if there is a heading too close to the end of the document.
|
||||||
|
|
||||||
|
What I've written here is probably long enough, but adding this final sentence can't hurt.
|
89
src/data/blog/examples/terminal-development.md
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
---
|
||||||
|
title: How Do I Develop My Terminal Portfolio Website with React
|
||||||
|
author: Sat Naing
|
||||||
|
pubDatetime: 2022-06-09T03:42:51Z
|
||||||
|
slug: how-do-i-develop-my-terminal-portfolio-website-with-react
|
||||||
|
featured: false
|
||||||
|
draft: false
|
||||||
|
tags:
|
||||||
|
- JavaScript
|
||||||
|
- ReactJS
|
||||||
|
- ContextAPI
|
||||||
|
- Styled-Components
|
||||||
|
- TypeScript
|
||||||
|
description:
|
||||||
|
"EXAMPLE POST: Developing a terminal-like website using ReactJS, TypeScript and Styled-Components.
|
||||||
|
Includes features like autocomplete, multiple themes, command hints etc."
|
||||||
|
timezone: "Asia/Yangon"
|
||||||
|
---
|
||||||
|
|
||||||
|
> This article is originally from my [blog post](https://satnaing.dev/blog/posts/how-do-i-develop-my-terminal-portfolio-website-with-react). I put this article to demonstrate how you can write blog posts/articles using AstroPaper theme.
|
||||||
|
|
||||||
|
Developing a terminal-like website using ReactJS, TypeScript and Styled-Components. Includes features like autocomplete, multiple themes, command hints etc.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Table of contents
|
||||||
|
|
||||||
|
## Intro
|
||||||
|
|
||||||
|
Recently, I've developed and published my portfolio + a blog. I’m glad I got some good feedback for it. Today, I want to introduce my new terminal-like portfolio website. It is developed using ReactJS, TypeScript. I got this idea from CodePen and YouTube.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
This project is a frontend project without any backend codes. The UI/UX part is designed in Figma. For the frontend user-interface, I chose React over pain JavaScript and NextJS. Why?
|
||||||
|
|
||||||
|
- Firstly, I want to write declarative code. Managing HTML DOM using JavaScript imperatively is really tedious.
|
||||||
|
- Secondly, because it is React!!! It is fast, and reliable.
|
||||||
|
- Lastly, I don’t need much of the SEO features, routing and image optimization provided by NextJS.
|
||||||
|
|
||||||
|
And of course there's TypeScript for type checking.
|
||||||
|
|
||||||
|
For styling, I took a different approach than what I usually do. Instead of choosing Pure CSS, Sass, or Utility CSS Framework like TailwindCSS, I chose the CSS-in-JS way (Styled-Components). Although I’ve known about Styled-Components for some time, I’ve never tried it out. So, the writing style and structures of Styled-Components in this project may not be very organized or very good.
|
||||||
|
|
||||||
|
This project doesn’t need very complex state management. I just use ContextAPI in this project for multiple theming and to avoid prop drilling.
|
||||||
|
|
||||||
|
Here’s a quick recap for the tech stack.
|
||||||
|
|
||||||
|
- Frontend: [ReactJS](https://reactjs.org/ "React Website"), [TypeScript](https://www.typescriptlang.org/ "TypeScript Website")
|
||||||
|
- Styling: [Styled-Components](https://styled-components.com/ "Styled-Components Website")
|
||||||
|
- UI/UX: [Figma](https://figma.com/ "Figma Website")
|
||||||
|
- State Management: [ContextAPI](https://reactjs.org/docs/context.html "React ContextAPI")
|
||||||
|
- Deployment: [Netlify](https://www.netlify.com/ "Netlify Website")
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
Here are some features of the project.
|
||||||
|
|
||||||
|
### Multiple Themes
|
||||||
|
|
||||||
|
Users can change multiple themes. At the time of writing this post, there are 5 themes; and more themes will probably be added in the future. The selected theme is saved in local storage so that the theme won’t change on page refresh.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Command-line Completion
|
||||||
|
|
||||||
|
To look and feel as close to the actual terminal as possible, I put a command-line completion feature which auto fills in partially typed commands by simply pressing ‘Tab’ or ‘Ctrl + i’.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Previous Commands
|
||||||
|
|
||||||
|
Users can go back to the previous commands or navigate the previously typed commands by pressing Up & Down Arrows.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### View/Clear Command History
|
||||||
|
|
||||||
|
previously typed commands can be viewed by typing ‘history’ in the command line. All the command history and terminal screen can be wiped out by typing ‘clear’ or pressing ‘Ctrl + l’.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Outro
|
||||||
|
|
||||||
|
This is a really fun project, and one special part of this project is I had to focus on logic rather than user-interface (even though this is kind of a frontend project).
|
||||||
|
|
||||||
|
## Project Links
|
||||||
|
|
||||||
|
- Website: [https://terminal.satnaing.dev/](https://terminal.satnaing.dev/ "https://terminal.satnaing.dev/")
|
||||||
|
- Repo: [https://github.com/satnaing/terminal-portfolio](https://github.com/satnaing/terminal-portfolio "https://github.com/satnaing/terminal-portfolio")
|
154
src/data/blog/how-to-add-latex-equations-in-blog-posts.md
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
---
|
||||||
|
author: Alberto Perdomo
|
||||||
|
pubDatetime: 2024-09-08T20:58:52.737Z
|
||||||
|
modDatetime: 2025-03-22T09:25:46.734Z
|
||||||
|
title: How to add LaTeX Equations in Astro blog posts
|
||||||
|
tags:
|
||||||
|
- docs
|
||||||
|
description: Learn how to add LaTeX equations in Astro blog posts using Markdown, KaTeX, and remark/rehype plugins.
|
||||||
|
---
|
||||||
|
|
||||||
|
This document demonstrates how to use LaTeX equations in your Markdown files for AstroPaper. LaTeX is a powerful typesetting system often used for mathematical and scientific documents.
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img
|
||||||
|
src="https://images.pexels.com/photos/22690748/pexels-photo-22690748/free-photo-of-close-up-of-complicated-equations-written-on-a-blackboard.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2"
|
||||||
|
alt="Free Close-up of complex equations on a chalkboard, showcasing chemistry and math symbols. Stock Photo"
|
||||||
|
/>
|
||||||
|
<figcaption class="text-center">
|
||||||
|
Photo by <a href="https://www.pexels.com/photo/close-up-of-complicated-equations-written-on-a-blackboard-22690748/">Vitaly Gariev</a>
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
## Table of contents
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
In this section, you will find instructions on how to add support for LaTeX in your Markdown files for AstroPaper.
|
||||||
|
|
||||||
|
1. Install the necessary remark and rehype plugins by running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install rehype-katex remark-math katex
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Update the Astro configuration (`astro.config.ts`) to use the these plugins:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// other imports
|
||||||
|
import remarkMath from "remark-math";
|
||||||
|
import rehypeKatex from "rehype-katex";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
// other configs
|
||||||
|
markdown: {
|
||||||
|
remarkPlugins: [
|
||||||
|
remarkMath, // <- new plugin
|
||||||
|
remarkToc,
|
||||||
|
[remarkCollapse, { test: "Table of contents" }],
|
||||||
|
],
|
||||||
|
rehypePlugins: [rehypeKatex], // <- new plugin
|
||||||
|
shikiConfig: {
|
||||||
|
// For more themes, visit https://shiki.style/themes
|
||||||
|
themes: { light: "min-light", dark: "night-owl" },
|
||||||
|
wrap: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// other configs
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Import KaTeX CSS in the main layout file `src/layouts/Layout.astro`
|
||||||
|
|
||||||
|
```astro
|
||||||
|
---
|
||||||
|
import { SITE } from "@config";
|
||||||
|
|
||||||
|
// astro code
|
||||||
|
---
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<!-- others... -->
|
||||||
|
<script is:inline src="/toggle-theme.js"></script>
|
||||||
|
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/katex@0.15.2/dist/katex.min.css"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<slot />
|
||||||
|
</body>
|
||||||
|
```
|
||||||
|
|
||||||
|
4. As the last step, add a text-color for `katex` in `src/styles/typography.css`.
|
||||||
|
|
||||||
|
```css
|
||||||
|
@plugin '@tailwindcss/typography';
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
/* other classes */
|
||||||
|
|
||||||
|
/* Katex text color */
|
||||||
|
.prose .katex-display {
|
||||||
|
@apply text-foreground;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Code Blocks & Syntax Highlighting ===== */
|
||||||
|
/* other classes */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And _voilà_, this setup allows you to write LaTeX equations in your Markdown files, which will be rendered properly when the site is built. Once you do it, the rest of the document will appear rendered correctly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Inline Equations
|
||||||
|
|
||||||
|
Inline equations are written between single dollar signs `$...$`. Here are some examples:
|
||||||
|
|
||||||
|
1. The famous mass-energy equivalence formula: `$E = mc^2$`
|
||||||
|
2. The quadratic formula: `$x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}$`
|
||||||
|
3. Euler's identity: `$e^{i\pi} + 1 = 0$`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Block Equations
|
||||||
|
|
||||||
|
For more complex equations or when you want the equation to be displayed on its own line, use double dollar signs `$$...$$`:
|
||||||
|
|
||||||
|
The Gaussian integral:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$$ \int_{-\infty}^{\infty} e^{-x^2} dx = \sqrt{\pi} $$
|
||||||
|
```
|
||||||
|
|
||||||
|
The definition of the Riemann zeta function:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$$ \zeta(s) = \sum_{n=1}^{\infty} \frac{1}{n^s} $$
|
||||||
|
```
|
||||||
|
|
||||||
|
Maxwell's equations in differential form:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$$
|
||||||
|
\begin{aligned}
|
||||||
|
\nabla \cdot \mathbf{E} &= \frac{\rho}{\varepsilon_0} \\
|
||||||
|
\nabla \cdot \mathbf{B} &= 0 \\
|
||||||
|
\nabla \times \mathbf{E} &= -\frac{\partial \mathbf{B}}{\partial t} \\
|
||||||
|
\nabla \times \mathbf{B} &= \mu_0\left(\mathbf{J} + \varepsilon_0 \frac{\partial \mathbf{E}}{\partial t}\right)
|
||||||
|
\end{aligned}
|
||||||
|
$$
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Using Mathematical Symbols
|
||||||
|
|
||||||
|
LaTeX provides a wide range of mathematical symbols:
|
||||||
|
|
||||||
|
- Greek letters: `$\alpha$`, `$\beta$`, `$\gamma$`, `$\delta$`, `$\epsilon$`, `$\pi$`
|
||||||
|
- Operators: `$\sum$`, `$\prod$`, `$\int$`, `$\partial$`, `$\nabla$`
|
||||||
|
- Relations: `$\leq$`, `$\geq$`, `$\approx$`, `$\sim$`, `$\propto$`
|
||||||
|
- Logical symbols: `$\forall$`, `$\exists$`, `$\neg$`, `$\wedge$`, `$\vee$`
|
184
src/data/blog/how-to-configure-astropaper-theme.md
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
---
|
||||||
|
author: Sat Naing
|
||||||
|
pubDatetime: 2022-09-23T04:58:53Z
|
||||||
|
modDatetime: 2025-03-20T03:15:57.792Z
|
||||||
|
title: How to configure AstroPaper theme
|
||||||
|
slug: how-to-configure-astropaper-theme
|
||||||
|
featured: true
|
||||||
|
draft: false
|
||||||
|
tags:
|
||||||
|
- configuration
|
||||||
|
- docs
|
||||||
|
description: How you can make AstroPaper theme absolutely yours.
|
||||||
|
---
|
||||||
|
|
||||||
|
AstroPaper is a highly customizable Astro blog theme. With AstroPaper, you can customize everything according to your personal taste. This article will explain how you can make some customizations easily in the config file.
|
||||||
|
|
||||||
|
## Table of contents
|
||||||
|
|
||||||
|
## Configuring SITE
|
||||||
|
|
||||||
|
The important configurations resides in `src/config.ts` file. Within that file, you'll see the `SITE` object where you can specify your website's main configurations.
|
||||||
|
|
||||||
|
During development, it's okay to leave `SITE.website` empty. But in production mode, you should specify your deployed url in `SITE.website` option since this will be used for canonical URL, social card URL etc.. which are important for SEO.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// file: src/config.ts
|
||||||
|
export const SITE = {
|
||||||
|
website: "https://astro-paper.pages.dev/", // replace this with your deployed domain
|
||||||
|
author: "Sat Naing",
|
||||||
|
profile: "https://satnaing.dev/",
|
||||||
|
desc: "A minimal, responsive and SEO-friendly Astro blog theme.",
|
||||||
|
title: "AstroPaper",
|
||||||
|
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: {
|
||||||
|
enabled: true,
|
||||||
|
text: "Suggest Changes",
|
||||||
|
url: "https://github.com/satnaing/astro-paper/edit/main/",
|
||||||
|
},
|
||||||
|
dynamicOgImage: true, // enable automatic dynamic og-image generation
|
||||||
|
lang: "en", // html lang code. Set this empty and default will be "en"
|
||||||
|
timezone: "Asia/Bangkok", // Default global timezone (IANA format) https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||||
|
} as const;
|
||||||
|
```
|
||||||
|
|
||||||
|
Here are SITE configuration options
|
||||||
|
|
||||||
|
| Options | Description |
|
||||||
|
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `website` | Your deployed website URL |
|
||||||
|
| `author` | Your name |
|
||||||
|
| `profile` | Your personal/portfolio website URL which is used for better SEO. Put `null` or empty string `""` if you don't have any. |
|
||||||
|
| `desc` | Your site description. Useful for SEO and social media sharing. |
|
||||||
|
| `title` | Your site name |
|
||||||
|
| `ogImage` | Your default OG image for the site. Useful for social media sharing. OG images can be an external image URL or they can be placed under `/public` directory. |
|
||||||
|
| `lightAndDarkMode` | Enable or disable `light & dark mode` for the website. If disabled, primary color scheme will be used. This option is enabled by default. |
|
||||||
|
| `postPerIndex` | The number of posts to be displayed at the home page under `Recent` section. |
|
||||||
|
| `postPerPage` | You can specify how many posts will be displayed in each posts page. (eg: if you set `SITE.postPerPage` to 3, each page will only show 3 posts per page) |
|
||||||
|
| `scheduledPostMargin` | In Production mode, posts with a future `pubDatetime` will not be visible. However, if a post's `pubDatetime` is within the next 15 minutes, it will be visible. You can set `scheduledPostMargin` if you don't like the default 15 minutes margin. |
|
||||||
|
| `showArchives` | Determines whether to display the `Archives` menu (positioned between the `About` and `Search` menus) and its corresponding page on the site. This option is set to `true` by default. |
|
||||||
|
| `showBackButton` | Determines whether to display the `Go back` button in each blog post. |
|
||||||
|
| `editPost` | This option allows users to suggest changes to a blog post by providing an edit link under blog post titles. This feature can be disabled by setting `SITE.editPost.enabled` to `false`. |
|
||||||
|
| `dynamicOgImage` | This option controls whether to [generate dynamic og-image](https://astro-paper.pages.dev/posts/dynamic-og-image-generation-in-astropaper-blog-posts/) if no `ogImage` is specified in the blog post frontmatter. If you have many blog posts, you might want to disable this feature. See the [trade-off](https://astro-paper.pages.dev/posts/dynamic-og-image-generation-in-astropaper-blog-posts/#trade-off) for more details. |
|
||||||
|
| `lang` | Used as HTML ISO Language code in `<html lang"en">`. Default is `en`. |
|
||||||
|
| `timezone` | This option allows you to specify your timezone using the [IANA format](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). Setting this ensures consistent timestamps across your localhost and deployed site, eliminating time differences. |
|
||||||
|
|
||||||
|
## Configuring logo or title
|
||||||
|
|
||||||
|
Prior to AstroPaper v5, you can update your site name/logo in `LOGO_IMAGE` object inside `src/config.ts` file. However, in AstroPaper v5, this option has been removed in favor of Astro's built-in SVG and Image components.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
There are 3 options you can do:
|
||||||
|
|
||||||
|
### Option 1: SITE title text
|
||||||
|
|
||||||
|
This is the easiest option. You just have to update `SITE.title` in `src/config.ts` file.
|
||||||
|
|
||||||
|
### Option 2: Astro's SVG component
|
||||||
|
|
||||||
|
You might want to use this option if you want to use an SVG logo.
|
||||||
|
|
||||||
|
- First add an SVG inside `src/assets` directory. (eg: `src/assets/dummy-logo.svg`)
|
||||||
|
- Then import that SVG inside `src/components/Header.astro`
|
||||||
|
|
||||||
|
```astro
|
||||||
|
---
|
||||||
|
// other imports
|
||||||
|
import DummyLogo from "@/assets/dummy-logo.svg";
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
- Finally, replace `{SITE.title}` with imported logo.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
class="absolute py-1 text-left text-2xl leading-7 font-semibold whitespace-nowrap sm:static"
|
||||||
|
>
|
||||||
|
<DummyLogo class="scale-75 dark:invert" />
|
||||||
|
<!-- {SITE.title} -->
|
||||||
|
</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
The best part of this approach is that you can customize your SVG styles as needed. In the example above, you can see how the SVG logo color can be inverted in dark mode.
|
||||||
|
|
||||||
|
### Option 3: Astro's Image component
|
||||||
|
|
||||||
|
If your logo is an image but not SVG, you can use Astro's Image component.
|
||||||
|
|
||||||
|
- Add your logo inside `src/assets` directory. (eg: `src/assets/dummy-logo.png`)
|
||||||
|
- Import `Image` and your logo in `src/components/Header.astro`
|
||||||
|
|
||||||
|
```astro
|
||||||
|
---
|
||||||
|
// other imports
|
||||||
|
import { Image } from "astro:assets";
|
||||||
|
import dummyLogo from "@/assets/dummy-logo.png";
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
- Then, replace `{SITE.title}` with imported logo.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
class="absolute py-1 text-left text-2xl leading-7 font-semibold whitespace-nowrap sm:static"
|
||||||
|
>
|
||||||
|
<image src="{dummyLogo}" alt="Dummy Blog" class="dark:invert" />
|
||||||
|
<!-- {SITE.title} -->
|
||||||
|
</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
With this approach, you can still adjust your image's appearance using CSS classes. However, this might not always fit what you want. If you need to display different logo images based on light or dark mode, check how light/dark icons are handled inside the `Header.astro` component.
|
||||||
|
|
||||||
|
## Configuring social links
|
||||||
|
|
||||||
|
You can configure social links in `SOCIALS` object inside `src/constants.ts`.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const SOCIALS = [
|
||||||
|
{
|
||||||
|
name: "Github",
|
||||||
|
href: "https://github.com/satnaing/astro-paper",
|
||||||
|
linkTitle: ` ${SITE.title} on Github`,
|
||||||
|
icon: IconGitHub,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "X",
|
||||||
|
href: "https://x.com/username",
|
||||||
|
linkTitle: `${SITE.title} on X`,
|
||||||
|
icon: IconBrandX,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "LinkedIn",
|
||||||
|
href: "https://www.linkedin.com/in/username/",
|
||||||
|
linkTitle: `${SITE.title} on LinkedIn`,
|
||||||
|
icon: IconLinkedin,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Mail",
|
||||||
|
href: "mailto:yourmail@gmail.com",
|
||||||
|
linkTitle: `Send an email to ${SITE.title}`,
|
||||||
|
icon: IconMail,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuring share links
|
||||||
|
|
||||||
|
You can configure share links in `SHARE_LINKS` object inside `src/constants.ts`.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
This is the brief specification of how you can customize this theme. You can customize more if you know some coding. For customizing styles, please read [this article](https://astro-paper.pages.dev/posts/customizing-astropaper-theme-color-schemes/). Thanks for reading.✌🏻
|
207
src/data/blog/how-to-integrate-giscus-comments.md
Normal file
|
@ -0,0 +1,207 @@
|
||||||
|
---
|
||||||
|
author: FjellOverflow
|
||||||
|
pubDatetime: 2024-07-25T11:11:53Z
|
||||||
|
modDatetime: 2025-03-12T12:28:53Z
|
||||||
|
title: How to integrate Giscus comments into AstroPaper
|
||||||
|
slug: how-to-integrate-giscus-comments
|
||||||
|
featured: false
|
||||||
|
draft: false
|
||||||
|
tags:
|
||||||
|
- astro
|
||||||
|
- blog
|
||||||
|
- docs
|
||||||
|
description: Comment function on a static blog hosted on GitHub Pages with Giscus.
|
||||||
|
---
|
||||||
|
|
||||||
|
Hosting a thin static blog on a platform like [GitHub Pages](https://docs.github.com/en/pages/getting-started-with-github-pages/creating-a-github-pages-site) has numerous advantages, but also takes away some interactivity. Fortunately, [Giscus](https://giscus.app/) exists and offers a way to embed user comments on static sites.
|
||||||
|
|
||||||
|
## Table of contents
|
||||||
|
|
||||||
|
## How _Giscus_ works
|
||||||
|
|
||||||
|
[Giscus uses the GitHub API](https://github.com/giscus/giscus?tab=readme-ov-file#how-it-works) to read and store comments made by _GitHub_ users in the `Discussions` associated with a repository.
|
||||||
|
|
||||||
|
Embed the _Giscus_ client-side script bundle on your site, configure it with the correct repository URL, and users can view and write comments (when logged into _GitHub_).
|
||||||
|
|
||||||
|
The approach is serverless, as the comments are stored on _GitHub_ and dynamically loaded from there on client side, hence perfect for a static blog, like _AstroPaper_.
|
||||||
|
|
||||||
|
## Setting up _Giscus_
|
||||||
|
|
||||||
|
_Giscus_ can be set up easily on [giscus.app](https://giscus.app/), but I will outline the process shortly still.
|
||||||
|
|
||||||
|
### Prequisites
|
||||||
|
|
||||||
|
Prequisites to get _Giscus_ working are
|
||||||
|
|
||||||
|
- the repository is [public](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/managing-repository-settings/setting-repository-visibility#making-a-repository-public)
|
||||||
|
- the [Giscus app](https://github.com/apps/giscus) is installed
|
||||||
|
- the [Discussions](https://docs.github.com/en/github/administering-a-repository/managing-repository-settings/enabling-or-disabling-github-discussions-for-a-repository) feature is turned on for your repository
|
||||||
|
|
||||||
|
If any of these conditions cannot be fulfilled for any reason, unfortunately, _Giscus_ cannot be integrated.
|
||||||
|
|
||||||
|
### Configuring _Giscus_
|
||||||
|
|
||||||
|
Next, configuring _Giscus_ is necessary. In most cases, the preselected defaults are suitable, and you should only modify them if you have a specific reason and know what you are doing. Don't worry too much about making the wrong choices; you can always adjust the configuration later on.
|
||||||
|
|
||||||
|
However you need to
|
||||||
|
|
||||||
|
- select the right language for the UI
|
||||||
|
- specify the _GitHub_ repository you want to connect, typically the repository containing your statically hosted _AstroPaper_ blog on _GitHub Pages_
|
||||||
|
- create and set an `Announcement` type discussion on _GitHub_ if you want to ensure nobody can create random comments directly on _GitHub_
|
||||||
|
- define the color scheme
|
||||||
|
|
||||||
|
After configuring the settings, _Giscus_ provides you with a generated `<script>` tag, which you will need in the next steps.
|
||||||
|
|
||||||
|
## Simple script tag
|
||||||
|
|
||||||
|
You should now have a script tag that looks like this:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script
|
||||||
|
src="https://giscus.app/client.js"
|
||||||
|
data-repo="[ENTER REPO HERE]"
|
||||||
|
data-repo-id="[ENTER REPO ID HERE]"
|
||||||
|
data-category="[ENTER CATEGORY NAME HERE]"
|
||||||
|
data-category-id="[ENTER CATEGORY ID HERE]"
|
||||||
|
data-mapping="pathname"
|
||||||
|
data-strict="0"
|
||||||
|
data-reactions-enabled="1"
|
||||||
|
data-emit-metadata="0"
|
||||||
|
data-input-position="bottom"
|
||||||
|
data-theme="preferred_color_scheme"
|
||||||
|
data-lang="en"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
async
|
||||||
|
></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
Simply add that to the source code of the site. Most likely, if you're using _AstroPaper_ and want to enable comments on posts, navigate to `src/layouts/PostDetails.astro` and paste it into the desired location where you want the comments to appear, perhaps underneath the `Share this post on:` buttons.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
<ShareLinks />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
+ <script src="https://giscus.app/client.js"
|
||||||
|
+ data-repo="[ENTER REPO HERE]"
|
||||||
|
+ data-repo-id="[ENTER REPO ID HERE]"
|
||||||
|
+ data-category="[ENTER CATEGORY NAME HERE]"
|
||||||
|
+ data-category-id="[ENTER CATEGORY ID HERE]"
|
||||||
|
+ ...
|
||||||
|
+ </script>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</Layout>
|
||||||
|
```
|
||||||
|
|
||||||
|
And it's done! You have successfully integrated comments in _AstroPaper_!
|
||||||
|
|
||||||
|
## React component with light/dark theme
|
||||||
|
|
||||||
|
The embedded script tag in the layout is quite static, with the _Giscus_ configuration, including `theme`, hardcoded into the layout. Given that _AstroPaper_ features a light/dark theme toggle, it would be nice for the comments to seamlessly transition between light and dark themes along with the rest of the site. To achieve this, a more sophisticated approach to embedding _Giscus_ is required.
|
||||||
|
|
||||||
|
Firstly, we are going to install the [React component](https://www.npmjs.com/package/@giscus/react) for _Giscus_:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm i @giscus/react && npx astro add react
|
||||||
|
```
|
||||||
|
|
||||||
|
Then we create a new `Comments.tsx` React component in `src/components`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import Giscus, { type Theme } from "@giscus/react";
|
||||||
|
import { GISCUS } from "@/constants";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface CommentsProps {
|
||||||
|
lightTheme?: Theme;
|
||||||
|
darkTheme?: Theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Comments({
|
||||||
|
lightTheme = "light",
|
||||||
|
darkTheme = "dark",
|
||||||
|
}: CommentsProps) {
|
||||||
|
const [theme, setTheme] = useState(() => {
|
||||||
|
const currentTheme = localStorage.getItem("theme");
|
||||||
|
const browserTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
||||||
|
.matches
|
||||||
|
? "dark"
|
||||||
|
: "light";
|
||||||
|
|
||||||
|
return currentTheme || browserTheme;
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
|
const handleChange = ({ matches }: MediaQueryListEvent) => {
|
||||||
|
setTheme(matches ? "dark" : "light");
|
||||||
|
};
|
||||||
|
|
||||||
|
mediaQuery.addEventListener("change", handleChange);
|
||||||
|
|
||||||
|
return () => mediaQuery.removeEventListener("change", handleChange);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const themeButton = document.querySelector("#theme-btn");
|
||||||
|
const handleClick = () => {
|
||||||
|
setTheme(prevTheme => (prevTheme === "dark" ? "light" : "dark"));
|
||||||
|
};
|
||||||
|
|
||||||
|
themeButton?.addEventListener("click", handleClick);
|
||||||
|
|
||||||
|
return () => themeButton?.removeEventListener("click", handleClick);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-8">
|
||||||
|
<Giscus theme={theme === "light" ? lightTheme : darkTheme} {...GISCUS} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This _React_ component not only wraps the native _Giscus_ component, but also introduces additional props, namely `lightTheme` and `darkTheme`. Leveraging two event listeners, the _Giscus_ comments will align with the site's theme, dynamically switching between dark and light themes whenever the site or browser theme is changed.
|
||||||
|
|
||||||
|
We also need to define the `GISCUS` config, for which the optimal location is in `src/constants.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { GiscusProps } from "@giscus/react";
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
export const GISCUS: GiscusProps = {
|
||||||
|
repo: "[ENTER REPO HERE]",
|
||||||
|
repoId: "[ENTER REPO ID HERE]",
|
||||||
|
category: "[ENTER CATEGORY NAME HERE]",
|
||||||
|
categoryId: "[ENTER CATEGORY ID HERE]",
|
||||||
|
mapping: "pathname",
|
||||||
|
reactionsEnabled: "0",
|
||||||
|
emitMetadata: "0",
|
||||||
|
inputPosition: "bottom",
|
||||||
|
lang: "en",
|
||||||
|
loading: "lazy",
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that specifying a `theme` here will override the `lightTheme` and `darkTheme` props, resulting in a static theme setting, similar to the previous approach of embedding _Giscus_ with the `<script>` tag.
|
||||||
|
|
||||||
|
To complete the process, add the new Comments component to `src/layouts/PostDetails.astro` (replacing the `script` tag from the previous step).
|
||||||
|
|
||||||
|
```diff
|
||||||
|
+ import Comments from "@/components/Comments";
|
||||||
|
|
||||||
|
<ShareLinks />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
+ <Comments client:only="react" />
|
||||||
|
|
||||||
|
<hr class="my-6 border-dashed" />
|
||||||
|
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</Layout>
|
||||||
|
```
|
||||||
|
|
||||||
|
And that's it!
|
114
src/data/blog/how-to-update-dependencies.md
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
---
|
||||||
|
title: How to update dependencies of AstroPaper
|
||||||
|
author: Sat Naing
|
||||||
|
pubDatetime: 2023-07-20T15:33:05.569Z
|
||||||
|
slug: how-to-update-dependencies
|
||||||
|
featured: false
|
||||||
|
draft: false
|
||||||
|
ogImage: ../../assets/images/forrest-gump-quote.png
|
||||||
|
tags:
|
||||||
|
- FAQ
|
||||||
|
description: How to update project dependencies and AstroPaper template.
|
||||||
|
---
|
||||||
|
|
||||||
|
Updating the dependencies of a project can be tedious. However, neglecting to update project dependencies is not a good idea either 😬. In this post, I will share how I usually update my projects, focusing on AstroPaper as an example. Nonetheless, these steps can be applied to other js/node projects as well.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Table of contents
|
||||||
|
|
||||||
|
## Updating Package Dependencies
|
||||||
|
|
||||||
|
There are several ways to update dependencies, and I've tried various methods to find the easiest path. One way to do it is by manually updating each package using `npm install package-name@latest`. This method is the most straightforward way of updating. However, it may not be the most efficient option.
|
||||||
|
|
||||||
|
My recommended way of updating dependencies is by using the [npm-check-updates package](https://www.npmjs.com/package/npm-check-updates). There's a good [article](https://www.freecodecamp.org/news/how-to-update-npm-dependencies/) from freeCodeCamp about that, so I won't be explaining the details of what it is and how to use that package. Instead, I'll show you my typical approach.
|
||||||
|
|
||||||
|
First, install `npm-check-updates` package globally.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g npm-check-updates
|
||||||
|
```
|
||||||
|
|
||||||
|
Before making any updates, it’s a good idea to check all new dependencies that can be updated.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ncu
|
||||||
|
```
|
||||||
|
|
||||||
|
Most of the time, patch dependencies can be updated without affecting the project at all. So, I usually update patch dependencies by running either `ncu -i --target patch` or `ncu -u --target patch`. The difference is that `ncu -u --target patch` will update all the patches, while `ncu -i --target patch` will give an option to toggle which package to update. It’s up to you to decide which approach to take.
|
||||||
|
|
||||||
|
The next part involves updating minor dependencies. Minor package updates usually won't break the project, but it is always good to check the release notes of the respective packages. These minor updates often include some cool features that can be applied to our projects.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ncu -i --target minor
|
||||||
|
```
|
||||||
|
|
||||||
|
Last but not least, there might be some major package updates in the dependencies. So, check the rest of the dependency updates by running
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ncu -i
|
||||||
|
```
|
||||||
|
|
||||||
|
If there are any major updates (or some updates you still have to make), the above command will output those remaining packages. If the package is a major version update, you have to be very careful since this will likely break the whole project. Therefore, please read the respective release note (or) docs very carefully and make changes accordingly.
|
||||||
|
|
||||||
|
If you run `ncu -i` and found no more packages to be updated, _**Congrats!!!**_ you have successfully updated all the dependencies in your project.
|
||||||
|
|
||||||
|
## Updating AstroPaper template
|
||||||
|
|
||||||
|
Like other open-source projects, AstroPaper is evolving with bug fixes, feature updates, and so on. So if you’re someone who is using AstroPaper as a template, you might also want to update the template when there’s a new release.
|
||||||
|
|
||||||
|
The thing is, you might already have updated the template according to your flavor. Therefore, I can’t exactly show **"the one-size-fits-all perfect way"** to update the template to the most recent release. However, here are some tips to update the template without breaking your repo. Keep in mind that, most of the time, updating the package dependencies might be sufficient for you.
|
||||||
|
|
||||||
|
### Files and Directories to keep in mind
|
||||||
|
|
||||||
|
In most cases, the files and directories you might not want to override (as you've likely updated those files) are `src/content/blog/`, `src/config.ts`, `src/pages/about.md`, and other assets & styles like `public/` and `src/styles/base.css`.
|
||||||
|
|
||||||
|
If you’re someone who only updates the bare minimum of the template, it should be okay to replace everything with the latest AstroPaper except the above files and directories. It’s like pure Android OS and other vendor-specific OSes like OneUI. The less you modify the base, the less you have to update.
|
||||||
|
|
||||||
|
You can manually replace every file one by one, or you can use the magic of git to update everything. I won’t show you the manual replacement process since it is very straightforward. If you’re not interested in that straightfoward and inefficient method, bear with me 🐻.
|
||||||
|
|
||||||
|
### Updating AstroPaper using Git
|
||||||
|
|
||||||
|
**IMPORTANT!!!**
|
||||||
|
|
||||||
|
> Only do the following if you know how to resolve merge conflicts. Otherwise, you’d better replace files manually or update dependencies only.
|
||||||
|
|
||||||
|
First, add astro-paper as the remote in your project.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git remote add astro-paper https://github.com/satnaing/astro-paper.git
|
||||||
|
```
|
||||||
|
|
||||||
|
Checkout to a new branch in order to update the template. If you know what you’re doing and you’re confident with your git skill, you can omit this step.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout -b build/update-astro-paper
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, pull the changes from astro-paper by running
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git pull astro-paper main
|
||||||
|
```
|
||||||
|
|
||||||
|
If you face `fatal: refusing to merge unrelated histories` error, you can resolve that by running the following command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git pull astro-paper main --allow-unrelated-histories
|
||||||
|
```
|
||||||
|
|
||||||
|
After running the above command, you’re likely to encounter conflicts in your project. You'll need to resolve these conflicts manually and make the necessary adjustments according to your needs.
|
||||||
|
|
||||||
|
After resolving the conflicts, test your blog thoroughly to ensure everything is working as expected. Check your articles, components, and any customizations you made.
|
||||||
|
|
||||||
|
Once you're satisfied with the result, it's time to merge the update branch into your main branch (only if you are updating the template in another branch). Congratulations! You've successfully updated your template to the latest version. Your blog is now up-to-date and ready to shine! 🎉
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
In this article, I've shared some of my insights and processes for updating dependencies and the AstroPaper template. I genuinely hope this article proves valuable and assists you in managing your projects more efficiently.
|
||||||
|
|
||||||
|
If you have any alternative or improved approaches for updating dependencies/AstroPaper, I would love to hear from you. Thus, don't hesitate to start a discussion in the repository, email me, or open an issue. Your input and ideas are highly appreciated!
|
||||||
|
|
||||||
|
Please understand that my schedule is quite busy these days, and I may not be able to respond quickly. However, I promise to get back to you as soon as possible. 😬
|
||||||
|
|
||||||
|
Thank you for taking the time to read this article, and I wish you all the best with your projects!
|
171
src/data/blog/predefined-color-schemes.md
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
---
|
||||||
|
author: Sat Naing
|
||||||
|
pubDatetime: 2022-09-26T12:13:24Z
|
||||||
|
modDatetime: 2024-01-04T09:09:06Z
|
||||||
|
title: Predefined color schemes
|
||||||
|
slug: predefined-color-schemes
|
||||||
|
featured: false
|
||||||
|
draft: false
|
||||||
|
tags:
|
||||||
|
- color-schemes
|
||||||
|
description:
|
||||||
|
Some of the well-crafted, predefined color schemes for AstroPaper blog
|
||||||
|
theme.
|
||||||
|
---
|
||||||
|
|
||||||
|
I've crafted some predefined color schemes for this AstroPaper blog theme. You can replace these color schemes with the original ones.
|
||||||
|
|
||||||
|
If you don't know how you can configure color schemes, check [this blog post](https://astro-paper.pages.dev/posts/customizing-astropaper-theme-color-schemes/).
|
||||||
|
|
||||||
|
## Table of contents
|
||||||
|
|
||||||
|
## Light color schemes
|
||||||
|
|
||||||
|
Light color scheme has to be defined using the css selector `:root` and `html[data-theme="light"]`.
|
||||||
|
|
||||||
|
### Lobster
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```css
|
||||||
|
:root,
|
||||||
|
html[data-theme="light"] {
|
||||||
|
--background: #f6eee1;
|
||||||
|
--foreground: #012c56;
|
||||||
|
--accent: #e14a39;
|
||||||
|
--muted: #efd8b0;
|
||||||
|
--border: #dc9891;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Leaf Blue
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```css
|
||||||
|
:root,
|
||||||
|
html[data-theme="light"] {
|
||||||
|
--background: #f2f5ec;
|
||||||
|
--foreground: #353538;
|
||||||
|
--accent: #1158d1;
|
||||||
|
--muted: #bbc789;
|
||||||
|
--border: #7cadff;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pinky light
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```css
|
||||||
|
:root,
|
||||||
|
html[data-theme="light"] {
|
||||||
|
--background: #fafcfc;
|
||||||
|
--foreground: #222e36;
|
||||||
|
--accent: #d3006a;
|
||||||
|
--muted: #f1bad4;
|
||||||
|
--border: #e3a9c6;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dark color schemes
|
||||||
|
|
||||||
|
Dark color scheme has to be defined as `html[data-theme="dark"]`.
|
||||||
|
|
||||||
|
### AstroPaper 1 original Dark Theme
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```css
|
||||||
|
html[data-theme="dark"] {
|
||||||
|
--background: #2f3741;
|
||||||
|
--foreground: #e6e6e6;
|
||||||
|
--accent: #1ad9d9;
|
||||||
|
--muted: #596b81;
|
||||||
|
--border: #3b4655;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deep Oyster
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```css
|
||||||
|
html[data-theme="dark"] {
|
||||||
|
--background: #21233d;
|
||||||
|
--foreground: #f4f7f5;
|
||||||
|
--accent: #ff5256;
|
||||||
|
--muted: #4a4e86;
|
||||||
|
--border: #b12f32;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pikky dark
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```css
|
||||||
|
html[data-theme="dark"] {
|
||||||
|
--background: #353640;
|
||||||
|
--foreground: #e9edf1;
|
||||||
|
--accent: #ff78c8;
|
||||||
|
--muted: #715566;
|
||||||
|
--border: #86436b;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Astro dark (High Contrast)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```css
|
||||||
|
html[data-theme="dark"] {
|
||||||
|
--background: #212737;
|
||||||
|
--foreground: #eaedf3;
|
||||||
|
--accent: #ff6b01;
|
||||||
|
--muted: #8a3302;
|
||||||
|
--border: #ab4b08;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Astro dark (New default dark theme in AstroPaper 2)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```css
|
||||||
|
html[data-theme="dark"] {
|
||||||
|
--background: #212737; /* lower contrast background */
|
||||||
|
--foreground: #eaedf3;
|
||||||
|
--accent: #ff6b01;
|
||||||
|
--muted: #8a3302;
|
||||||
|
--border: #ab4b08;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Astro Deep Purple (New dark theme in AstroPaper 3)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```css
|
||||||
|
html[data-theme="dark"] {
|
||||||
|
--background: #212737;
|
||||||
|
--foreground: #eaedf3;
|
||||||
|
--accent: #eb3fd3;
|
||||||
|
--muted: #7d4f7c;
|
||||||
|
--border: #642451;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### AstroPaper v4 Special (New dark theme in AstroPaper 4)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```css
|
||||||
|
html[data-theme="dark"] {
|
||||||
|
--background: #000123;
|
||||||
|
--accent: #617bff;
|
||||||
|
--foreground: #eaedf3;
|
||||||
|
--muted: #0c0e4f;
|
||||||
|
--border: #303f8a;
|
||||||
|
}
|
||||||
|
```
|
191
src/data/blog/setting-dates-via-git-hooks.md
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
---
|
||||||
|
author: Simon Smale
|
||||||
|
pubDatetime: 2024-01-03T20:40:08Z
|
||||||
|
modDatetime: 2024-01-08T18:59:05Z
|
||||||
|
title: How to use Git Hooks to set Created and Modified Dates
|
||||||
|
featured: false
|
||||||
|
draft: false
|
||||||
|
tags:
|
||||||
|
- docs
|
||||||
|
- FAQ
|
||||||
|
canonicalURL: https://smale.codes/posts/setting-dates-via-git-hooks/
|
||||||
|
description: How to use Git Hooks to set your Created and Modified Dates on AstroPaper
|
||||||
|
---
|
||||||
|
|
||||||
|
In this post I will explain how to use the pre-commit Git hook to automate the input of the created (`pubDatetime`) and modified (`modDatetime`) in the AstroPaper blog theme frontmatter
|
||||||
|
|
||||||
|
## Table of contents
|
||||||
|
|
||||||
|
## Have them Everywhere
|
||||||
|
|
||||||
|
[Git hooks](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks) are great for automating tasks like [adding](https://gist.github.com/SSmale/3b380e5bbed3233159fb7031451726ea) or [checking](https://itnext.io/using-git-hooks-to-enforce-branch-naming-policy-ffd81fa01e5e) the branch name to your commit messages or [stopping you committing plain text secrets](https://gist.github.com/SSmale/367deee757a9b2e119d241e120249000). Their biggest flaw is that client-side hooks are per machine.
|
||||||
|
|
||||||
|
You can get around this by having a `hooks` directory and manually copy them to the `.git/hooks` directory or set up a symlink, but this all requires you to remember to set it up, and that is not something I am good at doing.
|
||||||
|
|
||||||
|
As this project uses npm, we are able to make use of a package called [Husky](https://typicode.github.io/husky/) (this is already installed in AstroPaper) to automatically install the hooks for us.
|
||||||
|
|
||||||
|
> Update! In AstroPaper [v4.3.0](https://github.com/satnaing/astro-paper/releases/tag/v4.3.0), the pre-commit hook has been removed in favor of GitHub Actions. However, you can easily [install Husky](https://typicode.github.io/husky/get-started.html) yourself.
|
||||||
|
|
||||||
|
## The Hook
|
||||||
|
|
||||||
|
As we want this hook to run as we commit the code to update the dates and then have that as part of our change we are going to use the `pre-commit` hook. This has already been set up by this AstroPaper project, but if it hadn't, you would run `npx husky add .husky/pre-commit 'echo "This is our new pre-commit hook"'`.
|
||||||
|
|
||||||
|
Navigating to the `hooks/pre-commit` file, we are going to add one or both of the following snippets.
|
||||||
|
|
||||||
|
### Updating the modified date when a file is edited
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
UPDATE:
|
||||||
|
|
||||||
|
This section has been updated with a new version of the hook that is smarter. It will now not increment the `modDatetime` until the post is published. On the first publish, set the draft status to `first` and watch the magic happen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Modified files, update the modDatetime
|
||||||
|
git diff --cached --name-status |
|
||||||
|
grep -i '^M.*\.md$' |
|
||||||
|
while read _ file; do
|
||||||
|
filecontent=$(cat "$file")
|
||||||
|
frontmatter=$(echo "$filecontent" | awk -v RS='---' 'NR==2{print}')
|
||||||
|
draft=$(echo "$frontmatter" | awk '/^draft: /{print $2}')
|
||||||
|
if [ "$draft" = "false" ]; then
|
||||||
|
echo "$file modDateTime updated"
|
||||||
|
cat $file | sed "/---.*/,/---.*/s/^modDatetime:.*$/modDatetime: $(date -u "+%Y-%m-%dT%H:%M:%SZ")/" > tmp
|
||||||
|
mv tmp $file
|
||||||
|
git add $file
|
||||||
|
fi
|
||||||
|
if [ "$draft" = "first" ]; then
|
||||||
|
echo "First release of $file, draft set to false and modDateTime removed"
|
||||||
|
cat $file | sed "/---.*/,/---.*/s/^modDatetime:.*$/modDatetime:/" | sed "/---.*/,/---.*/s/^draft:.*$/draft: false/" > tmp
|
||||||
|
mv tmp $file
|
||||||
|
git add $file
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
`git diff --cached --name-status` gets the files from git that have been staged for committing. The output looks like:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
A src/content/blog/setting-dates-via-git-hooks.md
|
||||||
|
```
|
||||||
|
|
||||||
|
The letter at the start denotes what action has been taken, in the above example the file has been added. Modified files have `M`
|
||||||
|
|
||||||
|
We pipe that output into the grep command where we are looking at each line to find that have been modified. The line needs to start with `M` (`^(M)`), have any number of characters after that (`.*`) and end with the `.md` file extension (`.(md)$`).This is going to filter out the lines that are not modified markdown files `egrep -i "^(M).*\.(md)$"`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Improvement - More Explicit
|
||||||
|
|
||||||
|
This could be added to only look for files that we markdown files in the `blog` directory, as these are the only ones that will have the right frontmatter
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
The regex will capture the two parts, the letter and the file path. We are going to pipe this list into a while loop to iterate over the matching lines and assign the letter to `a` and the path to `b`. We are going to ignore `a` for now.
|
||||||
|
|
||||||
|
To know the draft staus of the file, we need its frontmatter. In the following code we are using `cat` to get the content of the file, then using `awk` to split the file on the frontmatter separator (`---`) and taking the second block (the fonmtmatter, the bit between the `---`). From here we are using `awk` again to find the draft key and print is value.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
filecontent=$(cat "$file")
|
||||||
|
frontmatter=$(echo "$filecontent" | awk -v RS='---' 'NR==2{print}')
|
||||||
|
draft=$(echo "$frontmatter" | awk '/^draft: /{print $2}')
|
||||||
|
```
|
||||||
|
|
||||||
|
Now we have the value for `draft` we are going to do 1 of 3 things, set the modDatetime to now (when draft is false `if [ "$draft" = "false" ]; then`), clear the modDatetime and set draft to false (when draft is set to first `if [ "$draft" = "first" ]; then`), or nothing (in any other case).
|
||||||
|
|
||||||
|
The next part with the sed command is a bit magical to me as I don't often use it, it was copied from [another blog post on doing something similar](https://mademistakes.com/notes/adding-last-modified-timestamps-with-git/). In essence, it is looking inside the frontmatter tags (`---`) of the file to find the `pubDatetime:` key, getting the full line and replacing it with the `pubDatetime: $(date -u "+%Y-%m-%dT%H:%M:%SZ")/"` same key again and the current datetime formatted correctly.
|
||||||
|
|
||||||
|
This replacement is in the context of the whole file so we put that into a temporary file (`> tmp`), then we move (`mv`) the new file into the location of the old file, overwriting it. This is then added to git ready to be committed as if we made the change ourselves.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### NOTE
|
||||||
|
|
||||||
|
For the `sed` to work the frontmatter needs to already have the `modDatetime` key in the frontmatter. There are some other changes you will need to make for the app to build with a blank date, see [further down](#empty-moddatetime-changes)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Adding the Date for new files
|
||||||
|
|
||||||
|
Adding the date for a new file is the same process as above, but this time we are looking for lines that have been added (`A`) and we are going to replace the `pubDatetime` value.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# New files, add/update the pubDatetime
|
||||||
|
git diff --cached --name-status | egrep -i "^(A).*\.(md)$" | while read a b; do
|
||||||
|
cat $b | sed "/---.*/,/---.*/s/^pubDatetime:.*$/pubDatetime: $(date -u "+%Y-%m-%dT%H:%M:%SZ")/" > tmp
|
||||||
|
mv tmp $b
|
||||||
|
git add $b
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Improvement - Only Loop Once
|
||||||
|
|
||||||
|
We could use the `a` variable to switch inside the loop and either update the `modDatetime` or add the `pubDatetime` in one loop.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Populating the frontmatter
|
||||||
|
|
||||||
|
If your IDE supports snippets then there is the option to create a custom snippet to populate the frontmatter.[In AstroPaper v4 will come with one for VSCode by default.](https://github.com/satnaing/astro-paper/pull/206)
|
||||||
|
|
||||||
|
<video autoplay muted="muted" controls plays-inline="true" class="border border-skin-line">
|
||||||
|
<source src="https://github.com/satnaing/astro-paper/assets/17761689/e13babbc-2d78-405d-8758-ca31915e41b0" type="video/mp4">
|
||||||
|
</video>
|
||||||
|
|
||||||
|
## Empty `modDatetime` changes
|
||||||
|
|
||||||
|
To allow Astro to compile the markdown and do its thing, it needs to know what is expected in the frontmatter. It does this via the config in `src/content/config.ts`
|
||||||
|
|
||||||
|
To allow the key to be there with no value we need to edit line 10 to add the `.nullable()` function.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const blog = defineCollection({
|
||||||
|
type: "content",
|
||||||
|
schema: ({ image }) =>
|
||||||
|
z.object({
|
||||||
|
author: z.string().default(SITE.author),
|
||||||
|
pubDatetime: z.date(),
|
||||||
|
- modDatetime: z.date().optional(),
|
||||||
|
+ 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(),
|
||||||
|
readingTime: z.string().optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
To stop the IDE complaining in the blog engine files I have also done the following:
|
||||||
|
|
||||||
|
1. added `| null` to line 15 in `src/layouts/Layout.astro` so that it looks like
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface Props {
|
||||||
|
title?: string;
|
||||||
|
author?: string;
|
||||||
|
description?: string;
|
||||||
|
ogImage?: string;
|
||||||
|
canonicalURL?: string;
|
||||||
|
pubDatetime?: Date;
|
||||||
|
modDatetime?: Date | null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- This needs to be 2 as it doesn't pick it up with the code block -->
|
||||||
|
|
||||||
|
2. added `| null` to line 5 in `src/components/Datetime.tsx` so that it looks like
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface DatetimesProps {
|
||||||
|
pubDatetime: string | Date;
|
||||||
|
modDatetime: string | Date | undefined | null;
|
||||||
|
}
|
||||||
|
```
|
28
src/layouts/AboutLayout.astro
Normal 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>
|
144
src/layouts/Layout.astro
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
---
|
||||||
|
import { ClientRouter } from "astro:transitions";
|
||||||
|
import { SITE } from "@/config";
|
||||||
|
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 ? `/${SITE.ogImage}` : "/og.png",
|
||||||
|
canonicalURL = new URL(Astro.url.pathname, Astro.url),
|
||||||
|
pubDatetime,
|
||||||
|
modDatetime,
|
||||||
|
scrollSmooth = false,
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
const socialImageURL = new URL(ogImage, 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=`${SITE.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="/favicon.svg" />
|
||||||
|
<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"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<slot />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
54
src/layouts/Main.astro
Normal 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>
|
287
src/layouts/PostDetails.astro
Normal file
|
@ -0,0 +1,287 @@
|
||||||
|
---
|
||||||
|
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 { getPath } from "@/utils/getPath";
|
||||||
|
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: initOgImage,
|
||||||
|
canonicalURL,
|
||||||
|
pubDatetime,
|
||||||
|
modDatetime,
|
||||||
|
timezone,
|
||||||
|
tags,
|
||||||
|
hideEditPost,
|
||||||
|
} = post.data;
|
||||||
|
|
||||||
|
const { Content } = await render(post);
|
||||||
|
|
||||||
|
let ogImageUrl: string | undefined;
|
||||||
|
|
||||||
|
// Determine OG image source
|
||||||
|
if (typeof initOgImage === "string") {
|
||||||
|
ogImageUrl = initOgImage; // Remote OG image (absolute URL)
|
||||||
|
} else if (initOgImage?.src) {
|
||||||
|
ogImageUrl = initOgImage.src; // Local asset
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use dynamic OG image if enabled and no remote|local ogImage
|
||||||
|
if (!ogImageUrl && SITE.dynamicOgImage) {
|
||||||
|
ogImageUrl = `${getPath(post.id, post.filePath)}/index.png`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve OG image URL (or fallback to SITE.ogImage / default `og.png`)
|
||||||
|
const ogImage = ogImageUrl
|
||||||
|
? new URL(ogImageUrl, Astro.url.origin).href
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const layoutProps = {
|
||||||
|
title: `${title} | ${SITE.title}`,
|
||||||
|
author,
|
||||||
|
description,
|
||||||
|
pubDatetime,
|
||||||
|
modDatetime,
|
||||||
|
canonicalURL,
|
||||||
|
ogImage,
|
||||||
|
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} {modDatetime} {timezone} size="lg" class="my-2" />
|
||||||
|
<EditPost class="max-sm:hidden" {hideEditPost} {post} />
|
||||||
|
</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" {hideEditPost} {post} />
|
||||||
|
|
||||||
|
<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>Back to Top</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>Previous Post</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>Next Post</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
|
@ -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>
|
36
src/pages/about.md
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
---
|
||||||
|
layout: ../layouts/AboutLayout.astro
|
||||||
|
title: "About"
|
||||||
|
---
|
||||||
|
|
||||||
|
AstroPaper is a minimal, responsive and SEO-friendly Astro blog theme. I designed and crafted this based on [my personal blog](https://satnaing.dev/blog).
|
||||||
|
|
||||||
|
This theme is aimed to be accessible out of the box. Light and dark mode are supported by
|
||||||
|
default and additional color schemes can also be configured.
|
||||||
|
|
||||||
|
This theme is self-documented \_ which means articles/posts in this theme can also be considered as documentations. So, see the documentation for more info.
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<img src="/dev.svg" class="sm:w-1/2 mx-auto" alt="coding dev illustration">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
This theme is written in vanilla JavaScript (+ TypeScript for type checking) and a little bit of ReactJS for some interactions. TailwindCSS is used for styling; and Markdown is used for blog contents.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
Here are certain features of this site.
|
||||||
|
|
||||||
|
- fully responsive and accessible
|
||||||
|
- SEO-friendly
|
||||||
|
- light & dark mode
|
||||||
|
- fuzzy search
|
||||||
|
- super fast performance
|
||||||
|
- draft posts
|
||||||
|
- pagination
|
||||||
|
- sitemap & rss feed
|
||||||
|
- highly customizable
|
||||||
|
|
||||||
|
If you like this theme, you can star/contribute to the [repo](https://github.com/satnaing/astro-paper).
|
||||||
|
Or you can even give any feedback via my [email](mailto:contact@satnaing.dev).
|
83
src/pages/archives/index.astro
Normal 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 = [
|
||||||
|
"January",
|
||||||
|
"February",
|
||||||
|
"March",
|
||||||
|
"April",
|
||||||
|
"May",
|
||||||
|
"June",
|
||||||
|
"July",
|
||||||
|
"August",
|
||||||
|
"September",
|
||||||
|
"October",
|
||||||
|
"November",
|
||||||
|
"December",
|
||||||
|
];
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title={`Archives | ${SITE.title}`}>
|
||||||
|
<Header />
|
||||||
|
<Main pageTitle="Archives" pageDesc="All the articles I've archived.">
|
||||||
|
{
|
||||||
|
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 => (
|
||||||
|
<Card {...data} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</Main>
|
||||||
|
<Footer />
|
||||||
|
</Layout>
|
121
src/pages/index.astro
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
---
|
||||||
|
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">
|
||||||
|
Mingalaba
|
||||||
|
</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>
|
||||||
|
AstroPaper is a minimal, responsive, accessible and SEO-friendly Astro
|
||||||
|
blog theme. This theme follows best practices and provides accessibility
|
||||||
|
out of the box. Light and dark mode are supported by default. Moreover,
|
||||||
|
additional color schemes can also be configured.
|
||||||
|
</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">Social Links:</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 => (
|
||||||
|
<Card variant="h3" {...data} />
|
||||||
|
))}
|
||||||
|
</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">Recent Posts</h2>
|
||||||
|
<ul>
|
||||||
|
{recentPosts.map(
|
||||||
|
(data, index) =>
|
||||||
|
index < SITE.postPerIndex && <Card variant="h3" {...data} />
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="my-8 text-center">
|
||||||
|
<LinkButton href="/posts/">
|
||||||
|
All 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>
|