├── .env.example
├── .gitignore
├── .vscode
├── extensions.json
├── launch.json
└── settings.json
├── LICENSE.md
├── README.md
├── astro.config.mjs
├── bun.lock
├── keystatic.config.ts
├── package.json
├── public
├── apple-touch-icon.png
├── favicon copy.svg
├── favicon-96x96.png
├── favicon.ico
├── favicon.svg
├── robots.txt
├── site.webmanifest
├── web-app-manifest-192x192.png
└── web-app-manifest-512x512.png
├── src
├── assets
│ ├── images
│ │ ├── Performances.png
│ │ ├── articles
│ │ │ ├── 10-tips-for-better-time-management
│ │ │ │ └── cover.avif
│ │ │ ├── a-function-s-existential-crisis
│ │ │ │ └── cover.avif
│ │ │ ├── exploring-the-benefits-of-daily-meditation
│ │ │ │ └── cover.avif
│ │ │ ├── how-to-build-a-capsule-wardrobe
│ │ │ │ └── cover.avif
│ │ │ ├── java-script-math-is-a-lie
│ │ │ │ └── cover.avif
│ │ │ ├── mastering-time-management
│ │ │ │ └── cover.avif
│ │ │ ├── promises-in-javascript-trust-issues-in-code
│ │ │ │ └── cover.avif
│ │ │ ├── the-art-of-minimalist-living
│ │ │ │ └── cover.avif
│ │ │ ├── the-basics-of-personal-finance
│ │ │ │ └── cover.avif
│ │ │ ├── the-benefits-of-meditation
│ │ │ │ └── cover.avif
│ │ │ ├── top-5-web-development-frameworks-for-2025
│ │ │ │ └── cover.avif
│ │ │ ├── understanding-java-script-closures
│ │ │ │ └── cover.avif
│ │ │ └── why-does-this-hate-me-demystifying-java-script-s-this
│ │ │ │ └── cover.avif
│ │ ├── authors
│ │ │ ├── ahmed-khan
│ │ │ │ └── avatar.jpg
│ │ │ ├── chloe-nguyen
│ │ │ │ └── avatar.jpg
│ │ │ ├── emily-devis
│ │ │ │ └── avatar.jpg
│ │ │ ├── jane-doe
│ │ │ │ └── avatar.jpg
│ │ │ ├── john-smith
│ │ │ │ └── avatar.jpg
│ │ │ ├── liam-leonard
│ │ │ │ └── avatar.jpg
│ │ │ ├── maria-gonzalez
│ │ │ │ └── avatar.jpg
│ │ │ ├── olivier-brown
│ │ │ │ └── avatar.jpg
│ │ │ ├── rajesh-patel
│ │ │ │ └── avatar.jpg
│ │ │ └── sofia-martinez
│ │ │ │ └── avatar.jpg
│ │ ├── default-avatar.jpg
│ │ ├── default-image.jpg
│ │ └── screenshot-astronews.png
│ └── svgs
│ │ ├── arrow-left-01.astro
│ │ ├── arrow-left-double.astro
│ │ ├── arrow-right-01.astro
│ │ ├── arrow-right-02.astro
│ │ ├── arrow-right-double.astro
│ │ ├── calendar-04.astro
│ │ ├── clipboard.astro
│ │ ├── facebook.astro
│ │ ├── github.astro
│ │ ├── hashtag.astro
│ │ ├── info.astro
│ │ ├── linkedin.astro
│ │ ├── menu.astro
│ │ ├── moon.astro
│ │ ├── new-twitter.astro
│ │ ├── pen-01.astro
│ │ ├── pencil-edit-01.astro
│ │ ├── resources-add.astro
│ │ ├── search-01.astro
│ │ ├── share-08.astro
│ │ ├── sun.astro
│ │ ├── telegram.astro
│ │ ├── time-04.astro
│ │ ├── twitter.astro
│ │ ├── user.astro
│ │ ├── whatsapp.astro
│ │ └── whatsapp.svg
├── components
│ ├── bases
│ │ ├── divider.astro
│ │ ├── head.astro
│ │ ├── icon.astro
│ │ ├── navbar-item.astro
│ │ ├── script.astro
│ │ ├── share-item.astro
│ │ └── theme-controller.astro
│ ├── cards
│ │ ├── authorCard.astro
│ │ ├── mainHeadline.astro
│ │ ├── newsCard.astro
│ │ ├── subHeadlineCard.astro
│ │ └── wideCard.astro
│ ├── elements
│ │ ├── menu-dropdown.astro
│ │ ├── navbar.astro
│ │ ├── share.astro
│ │ └── top-header.astro
│ └── shared
│ │ ├── Carousel.astro
│ │ ├── footer.astro
│ │ ├── header.astro
│ │ ├── pagination.astro
│ │ └── view-list-header.astro
├── content.config.ts
├── content
│ ├── articles
│ │ ├── 10-tips-for-better-time-management
│ │ │ └── index.mdx
│ │ ├── a-function-s-existential-crisis
│ │ │ └── index.mdx
│ │ ├── exploring-the-benefits-of-daily-meditation
│ │ │ └── index.mdx
│ │ ├── how-to-build-a-capsule-wardrobe
│ │ │ └── index.mdx
│ │ ├── javascript-math-is-a-lie
│ │ │ └── index.mdx
│ │ ├── mastering-time-management
│ │ │ └── index.mdx
│ │ ├── promises-in-javascript-trust-issues-in-code
│ │ │ └── index.mdx
│ │ ├── the-art-of-minimalist-living
│ │ │ └── index.mdx
│ │ ├── the-basics-of-personal-finance
│ │ │ └── index.mdx
│ │ ├── the-benefits-of-meditation
│ │ │ └── index.mdx
│ │ ├── top-5-web-development-frameworks-for-2025
│ │ │ └── index.mdx
│ │ ├── understanding-java-script-closures
│ │ │ └── index.mdx
│ │ └── why-does-this-hate-me-demystifying-java-script-s-this
│ │ │ └── index.mdx
│ ├── authors
│ │ ├── ahmed-khan
│ │ │ └── index.mdx
│ │ ├── chloe-nguyen
│ │ │ └── index.mdx
│ │ ├── emily-devis
│ │ │ └── index.mdx
│ │ ├── jane-doe
│ │ │ └── index.mdx
│ │ ├── john-smith
│ │ │ └── index.mdx
│ │ ├── liam-leonard
│ │ │ └── index.mdx
│ │ ├── maria-gonzalez
│ │ │ └── index.mdx
│ │ ├── olivier-brown
│ │ │ └── index.mdx
│ │ ├── rajesh-patel
│ │ │ └── index.mdx
│ │ └── sofia-martinez
│ │ │ └── index.mdx
│ ├── categories
│ │ ├── finance
│ │ │ └── index.json
│ │ ├── health
│ │ │ └── index.json
│ │ ├── lifestyle
│ │ │ └── index.json
│ │ ├── productivity
│ │ │ └── index.json
│ │ ├── programming
│ │ │ └── index.json
│ │ ├── technology
│ │ │ └── index.json
│ │ └── wellness
│ │ │ └── index.json
│ └── views
│ │ ├── about.mdx
│ │ ├── articles.mdx
│ │ ├── author.mdx
│ │ ├── authors.mdx
│ │ ├── categories.mdx
│ │ ├── contact.mdx
│ │ ├── error404.mdx
│ │ ├── home.mdx
│ │ └── search.mdx
├── layouts
│ ├── base.astro
│ ├── content.astro
│ └── list.astro
├── lib
│ ├── config
│ │ └── index.ts
│ ├── handlers
│ │ ├── articles.ts
│ │ ├── authors.ts
│ │ └── categories.ts
│ ├── keystatic
│ │ ├── articlesKs.ts
│ │ ├── authorsKs.ts
│ │ ├── categoriesKs.ts
│ │ └── index.ts
│ ├── schema
│ │ └── index.ts
│ ├── types
│ │ └── index.ts
│ └── utils
│ │ ├── date.ts
│ │ ├── getMeta.ts
│ │ ├── letter.ts
│ │ └── remarks.mjs
├── pages
│ ├── 404.astro
│ ├── _home
│ │ ├── authors.astro
│ │ ├── headerSection.astro
│ │ ├── headlines.astro
│ │ ├── latestNews.astro
│ │ └── news-ticker.astro
│ ├── about.astro
│ ├── articles
│ │ ├── [id].astro
│ │ ├── [page].astro
│ │ ├── _components
│ │ │ └── article-header.astro
│ │ └── index.astro
│ ├── authors
│ │ ├── [id]
│ │ │ ├── [page].astro
│ │ │ └── index.astro
│ │ └── index.astro
│ ├── categories
│ │ ├── [category]
│ │ │ ├── [page].astro
│ │ │ └── index.astro
│ │ └── index.astro
│ ├── index.astro
│ ├── rss.xml.js
│ └── search
│ │ └── index.astro
└── styles
│ └── global.css
└── tsconfig.json
/.env.example:
--------------------------------------------------------------------------------
1 | RUN_KEYSTATIC=false
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # build output
2 | dist/
3 |
4 | # generated types
5 | .astro/
6 |
7 | # dependencies
8 | node_modules/
9 |
10 | # logs
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 | pnpm-debug.log*
15 |
16 | # environment variables
17 | .env
18 | .env.production
19 |
20 | # macOS-specific files
21 | .DS_Store
22 |
23 | # jetbrains setting folder
24 | .idea/
25 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["astro-build.astro-vscode"],
3 | "unwantedRecommendations": []
4 | }
5 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "command": "./node_modules/.bin/astro dev",
6 | "name": "Development server",
7 | "request": "launch",
8 | "type": "node-terminal"
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.associations": {
3 | "*.css": "tailwindcss"
4 | },
5 | "editor.fontLigatures": "'calt', 'ss01', 'ss02', 'ss03', 'ss19', 'ss20'",
6 | "editor.fontFamily": "'Cascadia Code NF', Consolas, 'Courier New', monospace",
7 | "editor.tabSize": 2,
8 | "editor.formatOnSave": true
9 | }
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | MIT License
4 |
5 | Copyright (c) 2024 Mohammad Rahmani
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Astro News 📰
2 |
3 | A news website built with Astro, designed to provide a modern and responsive news reading experience.
4 |
5 | > **ℹ️ Info**
6 | > Astro news is my first open-source project, so there may be some bugs or issues. I’ll address them as quickly as possible.
7 |
8 | 
9 |
10 | 
11 |
12 | ## 🌐 Demo
13 |
14 | Explore the live demo here: [**Live Demo**](https://astro-news-six.vercel.app/)
15 |
16 | ## 🚀 Installation
17 |
18 | Follow the steps below to set up and run the project locally:
19 |
20 | ### Clone the Repository
21 |
22 | ```bash
23 | git clone https://github.com/Mrahmani71/astro-news.git
24 | ```
25 |
26 | ### Install Dependencies
27 |
28 | ``` bash
29 | bun install
30 | ```
31 |
32 | ### Run Development Server
33 |
34 | ```bash
35 | bun dev
36 | ```
37 |
38 | ### Running Keystatic CMS
39 |
40 | 1. Rename `.env.example` to `.env`.
41 |
42 | 2. Set `RUN_KEYSTATIC=true`.
43 |
44 | 3. Start the development server:
45 |
46 | ```bash
47 | bun dev
48 | ```
49 |
50 | 4. Open `http://localhost:4321/keystatic` in your browser.
51 |
52 | ## ✨ Features
53 |
54 | ### Implemented Features
55 |
56 | - Content Layer
57 | - Keystatic CMS
58 | - Navigation
59 | - Responsive Design
60 | - Pagination
61 | - Search Functionality
62 | - RSS Feed
63 | - Sitemap
64 | - Dark Mode
65 | - SEO Optimization (~)
66 |
67 | ### Upcoming Features
68 |
69 | - Open Graph (OG) Image Generation
70 |
71 | ## 💻 Technologies
72 |
73 | This project leverages cutting-edge web technologies:
74 |
75 | - [Astro V5.7](https://astro.build) - Modern static site builder
76 | - [KeyStatic](https://keystatic.com) - Headless content-management system
77 | - [Tailwind CSS](https://tailwindcss.com) - Utility-first CSS framework
78 | - [DaisyUI](https://daisyui.com/) - Tailwind CSS component library
79 | - [TypeScript](https://typescriptlang.org) - Typed JavaScript
80 | - [MDX](https://mdxjs.com) - Markdown with JSX support
81 | - [Bun V1.2.10](https://bun.sh) - Fast JavaScript runtime
82 | - [Vercel](https://vercel.com) - Deployment platform
83 | - [HugeIcons](https://hugeicons.com) - Icon library
84 |
85 | ## 💡 Inspirations and Code Concepts
86 |
87 | This project draws inspiration from the following sources:
88 |
89 | ### Designs
90 |
91 | - [BBC News](https://www.bbc.com)
92 | - [NewsHub - News Website](https://dribbble.com/shots/21678041-NewsHub-News-Website)
93 | - [Let'sread - News Landing Page](https://dribbble.com/shots/24675325-Let-sread-News-Landing-Page)
94 |
95 | ### Articles
96 |
97 | - [Creating A Pagination Component With Astro](https://rimdev.io/creating-a-pagination-component-with-astro)
98 | - [Adding search to static Astro sites](https://website-thomas-astro.vercel.app/blog/search-static-astro-website)
99 |
100 | ## 📄 License
101 |
102 | Open sourced under the [MIT license](LICENSE.md).
103 |
104 | ## 🤝 Contributing
105 |
106 | Contributions, issues, and feature requests are welcome! Feel free to check the [issues page](https://github.com/Mrahmani71/astro-news/issues).
107 |
--------------------------------------------------------------------------------
/astro.config.mjs:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import { defineConfig } from "astro/config";
3 | import tailwindcss from "@tailwindcss/vite";
4 | import mdx from "@astrojs/mdx";
5 | import sitemap from "@astrojs/sitemap";
6 | import { modifiedTime, readingTime } from "./src/lib/utils/remarks.mjs";
7 | import { SITE } from "./src/lib/config";
8 | import keystatic from "@keystatic/astro";
9 | import react from "@astrojs/react";
10 | import { loadEnv } from "vite";
11 | import pagefind from "astro-pagefind";
12 |
13 | const { RUN_KEYSTATIC } = loadEnv(import.meta.env.MODE, process.cwd(), "");
14 |
15 | const integrations = [mdx(), sitemap(), pagefind()];
16 |
17 | if (RUN_KEYSTATIC === "true") {
18 | integrations.push(react());
19 | integrations.push(keystatic());
20 | }
21 |
22 | // https://astro.build/config
23 | export default defineConfig({
24 | site: SITE.url,
25 | base: SITE.basePath,
26 | markdown: {
27 | remarkPlugins: [readingTime, modifiedTime],
28 | },
29 | experimental: {
30 | responsiveImages: true,
31 | },
32 | image: {},
33 | integrations,
34 | vite: {
35 | plugins: [tailwindcss()],
36 | },
37 | });
38 |
--------------------------------------------------------------------------------
/keystatic.config.ts:
--------------------------------------------------------------------------------
1 | import { articlesKs, authorsKs, categoriesKs } from "@/lib/keystatic";
2 | import { config } from "@keystatic/core";
3 |
4 | export default config({
5 | storage: {
6 | kind: "local",
7 | },
8 | ui: {
9 | brand: {
10 | name: "Astro News",
11 | },
12 | navigation: ["---", "articles", "---", "authors", "categories"],
13 | },
14 | collections: {
15 | articles: articlesKs,
16 | authors: authorsKs,
17 | categories: categoriesKs,
18 | },
19 | });
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "astro-news",
3 | "type": "module",
4 | "version": "0.0.1",
5 | "scripts": {
6 | "dev": "astro dev",
7 | "build": "astro build",
8 | "preview": "astro preview",
9 | "astro": "astro"
10 | },
11 | "dependencies": {
12 | "@astrojs/check": "^0.9.4",
13 | "@astrojs/mdx": "^4.2.4",
14 | "@astrojs/react": "^4.2.4",
15 | "@astrojs/rss": "^4.0.11",
16 | "@astrojs/sitemap": "^3.3.0",
17 | "@fontsource-variable/source-serif-4": "^5.2.6",
18 | "@fontsource/source-sans-pro": "^5.2.5",
19 | "@keystatic/astro": "^5.0.6",
20 | "@keystatic/core": "^0.5.47",
21 | "@pagefind/default-ui": "^1.3.0",
22 | "@tailwindcss/vite": "^4.1.4",
23 | "@types/react": "^19.1.2",
24 | "@types/react-dom": "^19.1.2",
25 | "astro": "^5.7.0",
26 | "astro-pagefind": "^1.8.3",
27 | "date-fns": "^4.1.0",
28 | "mdast-util-to-string": "^4.0.0",
29 | "pagefind": "^1.3.0",
30 | "react": "^19.1.0",
31 | "react-dom": "^19.1.0",
32 | "reading-time": "^1.5.0",
33 | "tailwindcss": "^4.1.4",
34 | "typescript": "^5.8.3"
35 | },
36 | "devDependencies": {
37 | "@tailwindcss/typography": "^0.5.16",
38 | "daisyui": "^5.0.20"
39 | }
40 | }
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrahmani71/astro-news/ed941bf3b30d42dc090beb49d5bce9db4ac3e0a1/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/favicon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrahmani71/astro-news/ed941bf3b30d42dc090beb49d5bce9db4ac3e0a1/public/favicon-96x96.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrahmani71/astro-news/ed941bf3b30d42dc090beb49d5bce9db4ac3e0a1/public/favicon.ico
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Allow: /
3 |
4 | Sitemap: https://astro-news-six.vercel.app/sitemap-index.xml
--------------------------------------------------------------------------------
/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Astro News",
3 | "short_name": "Astro News",
4 | "icons": [
5 | {
6 | "src": "/web-app-manifest-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png",
9 | "purpose": "maskable"
10 | },
11 | {
12 | "src": "/web-app-manifest-512x512.png",
13 | "sizes": "512x512",
14 | "type": "image/png",
15 | "purpose": "maskable"
16 | }
17 | ],
18 | "theme_color": "#ffffff",
19 | "background_color": "#ffffff",
20 | "display": "standalone"
21 | }
--------------------------------------------------------------------------------
/public/web-app-manifest-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrahmani71/astro-news/ed941bf3b30d42dc090beb49d5bce9db4ac3e0a1/public/web-app-manifest-192x192.png
--------------------------------------------------------------------------------
/public/web-app-manifest-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrahmani71/astro-news/ed941bf3b30d42dc090beb49d5bce9db4ac3e0a1/public/web-app-manifest-512x512.png
--------------------------------------------------------------------------------
/src/assets/images/Performances.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrahmani71/astro-news/ed941bf3b30d42dc090beb49d5bce9db4ac3e0a1/src/assets/images/Performances.png
--------------------------------------------------------------------------------
/src/assets/images/articles/10-tips-for-better-time-management/cover.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrahmani71/astro-news/ed941bf3b30d42dc090beb49d5bce9db4ac3e0a1/src/assets/images/articles/10-tips-for-better-time-management/cover.avif
--------------------------------------------------------------------------------
/src/assets/images/articles/a-function-s-existential-crisis/cover.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrahmani71/astro-news/ed941bf3b30d42dc090beb49d5bce9db4ac3e0a1/src/assets/images/articles/a-function-s-existential-crisis/cover.avif
--------------------------------------------------------------------------------
/src/assets/images/articles/exploring-the-benefits-of-daily-meditation/cover.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrahmani71/astro-news/ed941bf3b30d42dc090beb49d5bce9db4ac3e0a1/src/assets/images/articles/exploring-the-benefits-of-daily-meditation/cover.avif
--------------------------------------------------------------------------------
/src/assets/images/articles/how-to-build-a-capsule-wardrobe/cover.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrahmani71/astro-news/ed941bf3b30d42dc090beb49d5bce9db4ac3e0a1/src/assets/images/articles/how-to-build-a-capsule-wardrobe/cover.avif
--------------------------------------------------------------------------------
/src/assets/images/articles/java-script-math-is-a-lie/cover.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrahmani71/astro-news/ed941bf3b30d42dc090beb49d5bce9db4ac3e0a1/src/assets/images/articles/java-script-math-is-a-lie/cover.avif
--------------------------------------------------------------------------------
/src/assets/images/articles/mastering-time-management/cover.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrahmani71/astro-news/ed941bf3b30d42dc090beb49d5bce9db4ac3e0a1/src/assets/images/articles/mastering-time-management/cover.avif
--------------------------------------------------------------------------------
/src/assets/images/articles/promises-in-javascript-trust-issues-in-code/cover.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrahmani71/astro-news/ed941bf3b30d42dc090beb49d5bce9db4ac3e0a1/src/assets/images/articles/promises-in-javascript-trust-issues-in-code/cover.avif
--------------------------------------------------------------------------------
/src/assets/images/articles/the-art-of-minimalist-living/cover.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrahmani71/astro-news/ed941bf3b30d42dc090beb49d5bce9db4ac3e0a1/src/assets/images/articles/the-art-of-minimalist-living/cover.avif
--------------------------------------------------------------------------------
/src/assets/images/articles/the-basics-of-personal-finance/cover.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrahmani71/astro-news/ed941bf3b30d42dc090beb49d5bce9db4ac3e0a1/src/assets/images/articles/the-basics-of-personal-finance/cover.avif
--------------------------------------------------------------------------------
/src/assets/images/articles/the-benefits-of-meditation/cover.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrahmani71/astro-news/ed941bf3b30d42dc090beb49d5bce9db4ac3e0a1/src/assets/images/articles/the-benefits-of-meditation/cover.avif
--------------------------------------------------------------------------------
/src/assets/images/articles/top-5-web-development-frameworks-for-2025/cover.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrahmani71/astro-news/ed941bf3b30d42dc090beb49d5bce9db4ac3e0a1/src/assets/images/articles/top-5-web-development-frameworks-for-2025/cover.avif
--------------------------------------------------------------------------------
/src/assets/images/articles/understanding-java-script-closures/cover.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrahmani71/astro-news/ed941bf3b30d42dc090beb49d5bce9db4ac3e0a1/src/assets/images/articles/understanding-java-script-closures/cover.avif
--------------------------------------------------------------------------------
/src/assets/images/articles/why-does-this-hate-me-demystifying-java-script-s-this/cover.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrahmani71/astro-news/ed941bf3b30d42dc090beb49d5bce9db4ac3e0a1/src/assets/images/articles/why-does-this-hate-me-demystifying-java-script-s-this/cover.avif
--------------------------------------------------------------------------------
/src/assets/images/authors/ahmed-khan/avatar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrahmani71/astro-news/ed941bf3b30d42dc090beb49d5bce9db4ac3e0a1/src/assets/images/authors/ahmed-khan/avatar.jpg
--------------------------------------------------------------------------------
/src/assets/images/authors/chloe-nguyen/avatar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrahmani71/astro-news/ed941bf3b30d42dc090beb49d5bce9db4ac3e0a1/src/assets/images/authors/chloe-nguyen/avatar.jpg
--------------------------------------------------------------------------------
/src/assets/images/authors/emily-devis/avatar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrahmani71/astro-news/ed941bf3b30d42dc090beb49d5bce9db4ac3e0a1/src/assets/images/authors/emily-devis/avatar.jpg
--------------------------------------------------------------------------------
/src/assets/images/authors/jane-doe/avatar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrahmani71/astro-news/ed941bf3b30d42dc090beb49d5bce9db4ac3e0a1/src/assets/images/authors/jane-doe/avatar.jpg
--------------------------------------------------------------------------------
/src/assets/images/authors/john-smith/avatar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrahmani71/astro-news/ed941bf3b30d42dc090beb49d5bce9db4ac3e0a1/src/assets/images/authors/john-smith/avatar.jpg
--------------------------------------------------------------------------------
/src/assets/images/authors/liam-leonard/avatar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrahmani71/astro-news/ed941bf3b30d42dc090beb49d5bce9db4ac3e0a1/src/assets/images/authors/liam-leonard/avatar.jpg
--------------------------------------------------------------------------------
/src/assets/images/authors/maria-gonzalez/avatar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrahmani71/astro-news/ed941bf3b30d42dc090beb49d5bce9db4ac3e0a1/src/assets/images/authors/maria-gonzalez/avatar.jpg
--------------------------------------------------------------------------------
/src/assets/images/authors/olivier-brown/avatar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrahmani71/astro-news/ed941bf3b30d42dc090beb49d5bce9db4ac3e0a1/src/assets/images/authors/olivier-brown/avatar.jpg
--------------------------------------------------------------------------------
/src/assets/images/authors/rajesh-patel/avatar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrahmani71/astro-news/ed941bf3b30d42dc090beb49d5bce9db4ac3e0a1/src/assets/images/authors/rajesh-patel/avatar.jpg
--------------------------------------------------------------------------------
/src/assets/images/authors/sofia-martinez/avatar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrahmani71/astro-news/ed941bf3b30d42dc090beb49d5bce9db4ac3e0a1/src/assets/images/authors/sofia-martinez/avatar.jpg
--------------------------------------------------------------------------------
/src/assets/images/default-avatar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrahmani71/astro-news/ed941bf3b30d42dc090beb49d5bce9db4ac3e0a1/src/assets/images/default-avatar.jpg
--------------------------------------------------------------------------------
/src/assets/images/default-image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrahmani71/astro-news/ed941bf3b30d42dc090beb49d5bce9db4ac3e0a1/src/assets/images/default-image.jpg
--------------------------------------------------------------------------------
/src/assets/images/screenshot-astronews.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mrahmani71/astro-news/ed941bf3b30d42dc090beb49d5bce9db4ac3e0a1/src/assets/images/screenshot-astronews.png
--------------------------------------------------------------------------------
/src/assets/svgs/arrow-left-01.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { Icon } from "@/lib/types";
3 |
4 | type Props = Icon;
5 |
6 | const {
7 | width = "20",
8 | height = "20",
9 | color = "currentColor",
10 | strokeWidth = "1.5",
11 | size,
12 | } = Astro.props;
13 | ---
14 |
15 |
30 |
--------------------------------------------------------------------------------
/src/assets/svgs/arrow-left-double.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { Icon } from "@/lib/types";
3 |
4 | type Props = Icon;
5 |
6 | const {
7 | width = "20",
8 | height = "20",
9 | color = "currentColor",
10 | strokeWidth = "1.5",
11 | size,
12 | } = Astro.props;
13 | ---
14 |
15 |
36 |
--------------------------------------------------------------------------------
/src/assets/svgs/arrow-right-01.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { Icon } from "@/lib/types";
3 |
4 | type Props = Icon;
5 |
6 | const {
7 | width = "20",
8 | height = "20",
9 | color = "currentColor",
10 | strokeWidth = "1.5",
11 | size,
12 | } = Astro.props;
13 | ---
14 |
15 |
30 |
--------------------------------------------------------------------------------
/src/assets/svgs/arrow-right-02.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { Icon } from "@/lib/types";
3 |
4 | type Props = Icon;
5 |
6 | const {
7 | width = "20",
8 | height = "20",
9 | color = "currentColor",
10 | strokeWidth = "1.5",
11 | size,
12 | } = Astro.props;
13 | ---
14 |
15 |
36 |
--------------------------------------------------------------------------------
/src/assets/svgs/arrow-right-double.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { Icon } from "@/lib/types";
3 |
4 | type Props = Icon;
5 |
6 | const {
7 | width = "20",
8 | height = "20",
9 | color = "currentColor",
10 | strokeWidth = "1.5",
11 | size,
12 | } = Astro.props;
13 | ---
14 |
15 |
36 |
--------------------------------------------------------------------------------
/src/assets/svgs/calendar-04.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { Icon } from "@/lib/types";
3 |
4 | type Props = Icon;
5 |
6 | const {
7 | width = "20",
8 | height = "20",
9 | color = "currentColor",
10 | strokeWidth = "1.5",
11 | size,
12 | } = Astro.props;
13 | ---
14 |
15 |
42 |
--------------------------------------------------------------------------------
/src/assets/svgs/clipboard.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { Icon } from "@/lib/types";
3 |
4 | type Props = Icon;
5 |
6 | const {
7 | width = "20",
8 | height = "20",
9 | color = "currentColor",
10 | strokeWidth = "1.5",
11 | size,
12 | } = Astro.props;
13 | ---
14 |
15 |
38 |
--------------------------------------------------------------------------------
/src/assets/svgs/facebook.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { Icon } from "@/lib/types";
3 |
4 | type Props = Icon;
5 |
6 | const {
7 | width = "20",
8 | height = "20",
9 | color = "currentColor",
10 | strokeWidth = "1.5",
11 | size,
12 | } = Astro.props;
13 | ---
14 |
15 |
31 |
--------------------------------------------------------------------------------
/src/assets/svgs/github.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { Icon } from "@/lib/types";
3 |
4 | type Props = Icon;
5 |
6 | const {
7 | width = "20",
8 | height = "20",
9 | color = "currentColor",
10 | strokeWidth = "1.5",
11 | size,
12 | } = Astro.props;
13 | ---
14 |
15 |
36 |
--------------------------------------------------------------------------------
/src/assets/svgs/hashtag.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { Icon } from "@/lib/types";
3 |
4 | type Props = Icon;
5 |
6 | const { width = "20", height = "20", size } = Astro.props;
7 | ---
8 |
9 |
22 |
--------------------------------------------------------------------------------
/src/assets/svgs/info.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { Icon } from "@/lib/types";
3 |
4 | type Props = Icon;
5 |
6 | const { width = "20", height = "20", strokeWidth = "1.5", size } = Astro.props;
7 | ---
8 |
9 |
23 |
--------------------------------------------------------------------------------
/src/assets/svgs/linkedin.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { Icon } from "@/lib/types";
3 |
4 | type Props = Icon;
5 |
6 | const {
7 | width = "20",
8 | height = "20",
9 | color = "currentColor",
10 | strokeWidth = "1.5",
11 | size,
12 | } = Astro.props;
13 | ---
14 |
15 |
37 |
--------------------------------------------------------------------------------
/src/assets/svgs/menu.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { Icon } from "@/lib/types";
3 |
4 | type Props = Icon;
5 |
6 | const {
7 | width = "20",
8 | height = "20",
9 | color = "currentColor",
10 | strokeWidth = "1.5",
11 | size,
12 | } = Astro.props;
13 | ---
14 |
15 |
29 |
--------------------------------------------------------------------------------
/src/assets/svgs/moon.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { Icon } from "@/lib/types";
3 |
4 | type Props = Icon;
5 |
6 | const {
7 | width = "20",
8 | height = "20",
9 | color = "currentColor",
10 | strokeWidth = "1.5",
11 | size,
12 | } = Astro.props;
13 | ---
14 |
15 |
31 |
--------------------------------------------------------------------------------
/src/assets/svgs/new-twitter.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { Icon } from "@/lib/types";
3 |
4 | type Props = Icon;
5 |
6 | const {
7 | width = "20",
8 | height = "20",
9 | color = "currentColor",
10 | strokeWidth = "1.5",
11 | size,
12 | } = Astro.props;
13 | ---
14 |
15 |
30 |
--------------------------------------------------------------------------------
/src/assets/svgs/pen-01.astro:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | ---
4 |
5 |
26 |
--------------------------------------------------------------------------------
/src/assets/svgs/pencil-edit-01.astro:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | ---
4 |
5 |
25 |
--------------------------------------------------------------------------------
/src/assets/svgs/resources-add.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { Icon } from "@/lib/types";
3 |
4 | type Props = Icon;
5 |
6 | const {
7 | width = "20",
8 | height = "20",
9 | color = "currentColor",
10 | strokeWidth = "1.5",
11 | size,
12 | } = Astro.props;
13 | ---
14 |
15 |
42 |
--------------------------------------------------------------------------------
/src/assets/svgs/search-01.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { Icon } from "@/lib/types";
3 |
4 | type Props = Icon;
5 |
6 | const {
7 | width = "20",
8 | height = "20",
9 | color = "currentColor",
10 | strokeWidth = "1.5",
11 | size,
12 | } = Astro.props;
13 | ---
14 |
15 |
35 |
--------------------------------------------------------------------------------
/src/assets/svgs/share-08.astro:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | ---
4 |
5 |
30 |
--------------------------------------------------------------------------------
/src/assets/svgs/sun.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { Icon } from "@/lib/types";
3 |
4 | type Props = Icon;
5 |
6 | const {
7 | width = "20",
8 | height = "20",
9 | color = "currentColor",
10 | strokeWidth = "1.5",
11 | size,
12 | } = Astro.props;
13 | ---
14 |
15 |
35 |
--------------------------------------------------------------------------------
/src/assets/svgs/telegram.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { Icon } from "@/lib/types";
3 |
4 | type Props = Icon;
5 |
6 | const {
7 | width = "20",
8 | height = "20",
9 | color = "currentColor",
10 | strokeWidth = "1.5",
11 | size,
12 | } = Astro.props;
13 | ---
14 |
15 |
30 |
--------------------------------------------------------------------------------
/src/assets/svgs/time-04.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { Icon } from "@/lib/types";
3 |
4 | type Props = Icon;
5 |
6 | const {
7 | width = "20",
8 | height = "20",
9 | color = "currentColor",
10 | strokeWidth = "1.5",
11 | size,
12 | } = Astro.props;
13 | ---
14 |
15 |
32 |
--------------------------------------------------------------------------------
/src/assets/svgs/twitter.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { Icon } from "@/lib/types";
3 |
4 | type Props = Icon;
5 |
6 | const {
7 | width = "20",
8 | height = "20",
9 | color = "currentColor",
10 | strokeWidth = "1.5",
11 | size,
12 | } = Astro.props;
13 | ---
14 |
15 |
29 |
--------------------------------------------------------------------------------
/src/assets/svgs/user.astro:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | ---
4 |
5 |
24 |
--------------------------------------------------------------------------------
/src/assets/svgs/whatsapp.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { Icon } from "@/lib/types";
3 |
4 | type Props = Icon;
5 |
6 | const {
7 | width = "20",
8 | height = "20",
9 | color = "currentColor",
10 | strokeWidth = "1.5",
11 | size,
12 | } = Astro.props;
13 | ---
14 |
15 |
33 |
--------------------------------------------------------------------------------
/src/assets/svgs/whatsapp.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/bases/divider.astro:
--------------------------------------------------------------------------------
1 | ---
2 | type Props = {
3 | responsive?: boolean;
4 | };
5 |
6 | const { responsive = false } = Astro.props;
7 | ---
8 |
9 |
15 |
16 |
--------------------------------------------------------------------------------
/src/components/bases/head.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import "@/styles/global.css";
3 | import "@fontsource/source-sans-pro/400.css";
4 | import "@fontsource/source-sans-pro/600.css";
5 | import "@fontsource/source-sans-pro/700.css";
6 | import "@fontsource-variable/source-serif-4";
7 | import { ClientRouter } from "astro:transitions";
8 | import { SITE } from "@/lib/config";
9 | import type { ArticleMeta, Meta } from "@/lib/types";
10 |
11 | type Props = {
12 | meta: Meta | ArticleMeta;
13 | };
14 |
15 | const { meta } = Astro.props;
16 |
17 | // Type guard to check if props is ArticleMeta
18 | const isArticleMeta = (props: Props["meta"]): props is ArticleMeta =>
19 | props.type === "article";
20 | const canonicalURL = new URL(Astro.url.pathname, Astro.site).href;
21 |
22 | const OGImage = new URL(meta.ogImage, Astro.url).href;
23 | ---
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
47 |
48 |
49 |
50 |
51 |
52 | {meta.title}
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | {
74 | isArticleMeta(meta) ? (
75 | <>
76 |
80 |
84 |
85 | {meta.authors.map((author) => (
86 | <>
87 |
88 |
92 | >
93 | ))}
94 | >
95 | ) : null
96 | }
97 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/src/components/bases/icon.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Github from "@/assets/svgs/github.astro";
3 | import NewTwitter from "@/assets/svgs/new-twitter.astro";
4 | import Telegram from "@/assets/svgs/telegram.astro";
5 | import Facebook from "@/assets/svgs/facebook.astro";
6 |
7 | export interface Props {
8 | icon: string | undefined;
9 | }
10 |
11 | const { icon } = Astro.props;
12 |
13 | if (!icon) {
14 | throw new Error("Icon prop is required");
15 | }
16 | ---
17 |
18 | {icon === "github" && }
19 | {icon === "telegram" && }
20 | {icon === "newTwitter" && }
21 | {icon === "facebook" && }
22 |
--------------------------------------------------------------------------------
/src/components/bases/navbar-item.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { Link } from "@/lib/types";
3 |
4 | type Props = {
5 | item: Link;
6 | };
7 | const currentPath = Astro.url.pathname;
8 | const { item } = Astro.props;
9 |
10 | function isActive(item: Link, currentPath: string) {
11 | const segment = currentPath.split("/")[2];
12 | return (
13 | (item.text === "Home" && currentPath === "/") ||
14 | (item.text === "Articles" &&
15 | segment !== undefined &&
16 | !Number.isNaN(Number(segment)) &&
17 | Number(segment) >= 1) ||
18 | (item.text !== "Articles" &&
19 | currentPath.split("/").includes(item.text.toLocaleLowerCase()))
20 | );
21 | }
22 |
23 | function formatHref(href: string) {
24 | return href === "/" ? "/" : `${href}/1`;
25 | }
26 | ---
27 |
28 |
36 | {item.text}
37 |
38 |
--------------------------------------------------------------------------------
/src/components/bases/script.astro:
--------------------------------------------------------------------------------
1 |
55 |
--------------------------------------------------------------------------------
/src/components/bases/share-item.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Clipboard from "@/assets/svgs/clipboard.astro";
3 | import Facebook from "@/assets/svgs/facebook.astro";
4 | import Linkedin from "@/assets/svgs/linkedin.astro";
5 | import Twitter from "@/assets/svgs/twitter.astro";
6 | import Whatsapp from "@/assets/svgs/whatsapp.astro";
7 |
8 | type Props = {
9 | className: string;
10 | title: string;
11 | dataAwSocialShare: string;
12 | dataAwUrl: string | URL;
13 | dataAwText: string;
14 | icon: "clipboard" | "facebook" | "linkedin" | "twitter" | "whatsapp";
15 | };
16 |
17 | const { className, title, dataAwSocialShare, dataAwUrl, dataAwText, icon } =
18 | Astro.props;
19 |
20 | const iconMap = {
21 | clipboard: Clipboard,
22 | facebook: Facebook,
23 | linkedin: Linkedin,
24 | twitter: Twitter,
25 | whatsapp: Whatsapp,
26 | } as const;
27 |
28 | const Icon = iconMap[icon];
29 | ---
30 |
31 |
45 |
46 |
62 |
--------------------------------------------------------------------------------
/src/components/bases/theme-controller.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Moon from "@/assets/svgs/moon.astro";
3 | import Sun from "@/assets/svgs/sun.astro";
4 | ---
5 |
6 |
20 |
21 |
54 |
--------------------------------------------------------------------------------
/src/components/cards/authorCard.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { Image } from "astro:assets";
3 | import type { CollectionEntry } from "astro:content";
4 | type Props = {
5 | author: CollectionEntry<"authors">;
6 | };
7 |
8 | const { author } = Astro.props;
9 | ---
10 |
11 |
12 |
21 |
22 |
28 |
{author.data.job}
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/components/cards/mainHeadline.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { Image } from "astro:assets";
3 | import type { CollectionEntry } from "astro:content";
4 | import { render } from "astro:content";
5 | import Divider from "@/components/bases/divider.astro";
6 | import { categoriesHandler } from "@/lib/handlers/categories";
7 | import { getDateDistance } from "@/lib/utils/date";
8 |
9 | type Props = {
10 | article: CollectionEntry<"articles">;
11 | };
12 |
13 | const { article } = Astro.props;
14 |
15 | const { remarkPluginFrontmatter } = await render(article);
16 |
17 | const category = categoriesHandler.oneCategory(article.data.category.id);
18 | ---
19 |
20 |
21 |
22 |
30 |
31 |
32 |
33 |
41 |
42 | {article.data.description}
43 |
44 |
45 |
46 |
{category.data.title}
48 |
49 |
{getDateDistance(remarkPluginFrontmatter.lastModified)}
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/src/components/cards/newsCard.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { Image } from "astro:assets";
3 | import Divider from "../bases/divider.astro";
4 | import type { CollectionEntry } from "astro:content";
5 | import { render } from "astro:content";
6 | import { categoriesHandler } from "@/lib/handlers/categories";
7 |
8 | type Props = {
9 | article: CollectionEntry<"articles">;
10 | index: number;
11 | };
12 |
13 | const { article, index } = Astro.props;
14 |
15 | const { remarkPluginFrontmatter } = await render(article);
16 | const category = categoriesHandler.oneCategory(article.data.category.id);
17 | ---
18 |
19 |
22 |
23 |
32 |
33 |
34 |
42 |
43 | {article.data.description}
44 |
45 |
46 |
{category.data.title}
49 |
50 |
{remarkPluginFrontmatter.minutesRead}
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/src/components/cards/subHeadlineCard.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { Image } from "astro:assets";
3 | import type { CollectionEntry } from "astro:content";
4 | import Divider from "@/components/bases/divider.astro";
5 | import { render } from "astro:content";
6 | import { getDateDistance } from "@/lib/utils/date";
7 | import { categoriesHandler } from "@/lib/handlers/categories";
8 |
9 | type Props = {
10 | article: CollectionEntry<"articles">;
11 | isFirst?: boolean;
12 | isLast?: boolean;
13 | };
14 | const { article, isLast, isFirst } = Astro.props;
15 |
16 | const { remarkPluginFrontmatter } = await render(article);
17 | const category = categoriesHandler.oneCategory(article.data.category.id);
18 | ---
19 |
20 |
27 |
28 |
29 |
30 |
36 |
39 | {article.data.description}
40 |
41 |
42 |
43 |
44 |
{category.data.title}
46 |
47 |
{getDateDistance(remarkPluginFrontmatter.lastModified)}
48 |
49 |
50 |
51 |
54 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/src/components/cards/wideCard.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { Image } from "astro:assets";
3 | import type { CollectionEntry } from "astro:content";
4 | import { getDateDistance, normalizeDate } from "@/lib/utils/date";
5 | import Divider from "../bases/divider.astro";
6 |
7 | type Props = {
8 | article: CollectionEntry<"articles">;
9 | isLast: boolean;
10 | };
11 |
12 | const { article, isLast } = Astro.props;
13 | ---
14 |
15 |
18 |
21 | {getDateDistance(normalizeDate(article.data.publishedTime))}
22 |
23 |
29 |
30 |
39 |
40 |
41 |
47 |
48 | {article.data.description}
49 |
50 |
51 |
52 |
60 |
61 |
62 |
63 | {article.data.category.id}
64 |
65 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/src/components/elements/menu-dropdown.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Menu from "@/assets/svgs/menu.astro";
3 | import { NAVIGATION_LINKS, OTHER_LINKS } from "@/lib/config";
4 | ---
5 |
6 |
7 |
15 |
16 |
20 | - Home
21 | - Articles
22 |
23 | -
24 |
25 | Categories
26 |
27 | {
28 | NAVIGATION_LINKS.map(({ href, text, target }) => (
29 | -
30 |
31 | {text}
32 |
33 |
34 | ))
35 | }
36 |
37 |
38 |
39 |
40 | -
41 |
42 | Other Pages
43 |
44 | {
45 | OTHER_LINKS.map(({ href, text, target }) => (
46 | -
47 |
48 | {text}
49 |
50 |
51 | ))
52 | }
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/src/components/elements/navbar.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { NAVIGATION_LINKS } from "@/lib/config";
3 | import NavbarItem from "../bases/navbar-item.astro";
4 | ---
5 |
6 |
13 |
--------------------------------------------------------------------------------
/src/components/elements/share.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import ShareItem from "@/components/bases/share-item.astro";
3 | import ScriptShare from "@/components/bases/script.astro";
4 | type Props = {
5 | text: string;
6 | };
7 | const { pathname } = Astro.url;
8 | const { text } = Astro.props;
9 | ---
10 |
11 | <>
12 |
13 |
21 |
29 |
30 |
38 |
39 |
47 |
48 |
56 |
57 |
58 | >
59 |
--------------------------------------------------------------------------------
/src/components/elements/top-header.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Github from "@/assets/svgs/github.astro";
3 | import Search01 from "@/assets/svgs/search-01.astro";
4 | import MenuDropdown from "./menu-dropdown.astro";
5 | import { SITE } from "@/lib/config";
6 | import ThemeController from "../bases/theme-controller.astro";
7 | ---
8 |
9 |
34 |
--------------------------------------------------------------------------------
/src/components/shared/Carousel.astro:
--------------------------------------------------------------------------------
1 | ---
2 | // Carousel.astro
3 | interface Props {
4 | images: {
5 | src: string;
6 | alt: string;
7 | }[];
8 | autoplay?: boolean;
9 | interval?: number;
10 | }
11 |
12 | const {
13 | images = [
14 | { src: "/placeholder.svg?height=400&width=800", alt: "Slide 1" },
15 | { src: "/placeholder.svg?height=400&width=800", alt: "Slide 2" },
16 | { src: "/placeholder.svg?height=400&width=800", alt: "Slide 3" },
17 | ],
18 | autoplay = true,
19 | interval = 5000,
20 | } = Astro.props as Props;
21 | ---
22 |
23 |
29 |
30 |
31 | {
32 | images.map((image, index) => (
33 |
44 | ))
45 | }
46 |
47 |
48 |
49 |
50 | {
51 | images.map((_, index) => (
52 |
59 | ))
60 | }
61 |
62 |
63 |
64 |
89 |
90 |
--------------------------------------------------------------------------------
/src/components/shared/footer.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { NAVIGATION_LINKS, SOCIAL_LINKS, OTHER_LINKS } from "@/lib/config";
3 | import Hashtag from "@/assets/svgs/hashtag.astro";
4 | import Icon from "../bases/icon.astro";
5 | ---
6 |
7 |
74 |
--------------------------------------------------------------------------------
/src/components/shared/header.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import TopHeader from "../elements/top-header.astro";
3 | import Navbar from "../elements/navbar.astro";
4 | ---
5 |
6 |
12 |
--------------------------------------------------------------------------------
/src/components/shared/pagination.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import ArrowLeftDouble from "@/assets/svgs/arrow-left-double.astro";
3 | import ArrowRightDouble from "@/assets/svgs/arrow-right-double.astro";
4 | import ArrowLeft01 from "@/assets/svgs/arrow-left-01.astro";
5 | import ArrowRight01 from "@/assets/svgs/arrow-right-01.astro";
6 |
7 | type Props = {
8 | length: number;
9 | currentUrl: string;
10 | currentPage: number;
11 | baseUrl: string;
12 | prevUrl: string | undefined;
13 | nextUrl: string | undefined;
14 | lastUrl: string;
15 | };
16 |
17 | const { length, currentUrl, currentPage, baseUrl, prevUrl, nextUrl, lastUrl } =
18 | Astro.props;
19 |
20 | // Define the maximum number of visible buttons
21 | const maxVisibleButtons = 4;
22 |
23 | // Calculate the range of visible page numbers
24 | const startPage = Math.max(1, currentPage - Math.floor(maxVisibleButtons / 2));
25 | const endPage = Math.min(length, startPage + maxVisibleButtons - 1);
26 |
27 | // Adjust the startPage if we're at the end of the pagination
28 | const adjustedStartPage = Math.max(1, endPage - maxVisibleButtons + 1);
29 |
30 | // Generate the pagination list based on the range
31 | const paginationList = Array.from(
32 | { length: endPage - adjustedStartPage + 1 },
33 | (_, i) => adjustedStartPage + i
34 | );
35 | ---
36 |
37 |
43 |
107 |
108 |
--------------------------------------------------------------------------------
/src/components/shared/view-list-header.astro:
--------------------------------------------------------------------------------
1 | ---
2 | type Props = {
3 | title: string;
4 | };
5 | const { title } = Astro.props;
6 | ---
7 |
8 |
13 |
--------------------------------------------------------------------------------
/src/content.config.ts:
--------------------------------------------------------------------------------
1 | import { glob } from "astro/loaders";
2 | import { defineCollection } from "astro:content";
3 | import {
4 | articleSchema,
5 | authorSchema,
6 | categorySchema,
7 | viewSchema,
8 | } from "@/lib/schema";
9 |
10 | const articleCollection = defineCollection({
11 | loader: glob({ pattern: "**/*.mdx", base: "./src/content/articles" }),
12 | schema: ({ image }) => articleSchema(image),
13 | });
14 |
15 | const viewCollection = defineCollection({
16 | loader: glob({ pattern: "**/*.mdx", base: "./src/content/views" }),
17 | schema: viewSchema,
18 | });
19 |
20 | const categoryCollection = defineCollection({
21 | loader: glob({ pattern: "**/index.json", base: "./src/content/categories" }),
22 | schema: categorySchema,
23 | });
24 |
25 | const authorCollection = defineCollection({
26 | loader: glob({ pattern: "**/index.mdx", base: "./src/content/authors" }),
27 | schema: ({ image }) => authorSchema(image),
28 | });
29 |
30 | export const collections = {
31 | articles: articleCollection,
32 | views: viewCollection,
33 | categories: categoryCollection,
34 | authors: authorCollection,
35 | };
36 |
--------------------------------------------------------------------------------
/src/content/articles/10-tips-for-better-time-management/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | isDraft: false
3 | isMainHeadline: false
4 | isSubHeadline: true
5 | description: Learn how to optimize your schedule and accomplish more each day.
6 | title: 10 Tips for Better Time Management
7 | cover: '@assets/images/articles/10-tips-for-better-time-management/cover.avif'
8 | category: productivity
9 | publishedTime: 2024-11-14T00:00:00.000Z
10 | authors:
11 | - olivier-brown
12 | - jane-doe
13 | ---
14 | Time management is essential for achieving your goals efficiently. Here are ten practical tips to make the most of your time.
15 |
16 | ## 1. Prioritize Your Tasks
17 |
18 | Use a method like the Eisenhower Matrix to identify urgent and important tasks.
19 |
20 | ...
21 |
--------------------------------------------------------------------------------
/src/content/articles/a-function-s-existential-crisis/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | isDraft: false
3 | isMainHeadline: false
4 | isSubHeadline: false
5 | description: >-
6 | A lighthearted guide to recursion—aka that thing you call within itself until
7 | it forgets why it started.
8 | title: A Functions Existential Crisis
9 | cover: '@assets/images/articles/a-function-s-existential-crisis/cover.avif'
10 | category: programming
11 | publishedTime: 2025-04-14T23:39:00.000Z
12 | authors:
13 | - jane-doe
14 | ---
15 | Welcome to recursion, where functions call themselves like lost souls trying to find meaning. We’ll cover what recursion is, how it works, and how to keep your brain from crashing like your code.
16 |
--------------------------------------------------------------------------------
/src/content/articles/exploring-the-benefits-of-daily-meditation/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | isDraft: false
3 | isMainHeadline: false
4 | isSubHeadline: true
5 | description: >-
6 | Discover how a daily meditation practice can improve your mental clarity and
7 | well-being.
8 | title: Exploring the Benefits of Daily Meditation
9 | cover: '@assets/images/articles/exploring-the-benefits-of-daily-meditation/cover.avif'
10 | category: wellness
11 | publishedTime: 2024-11-18T00:00:00.000Z
12 | authors:
13 | - emily-devis
14 | ---
15 | Meditation is a simple practice with profound benefits. Learn how dedicating just a few minutes each day can transform your life.
16 |
17 | ## What Is Meditation?
18 |
19 | Meditation is the art of focusing your mind and achieving a heightened state of awareness.
20 |
21 | ...
22 |
--------------------------------------------------------------------------------
/src/content/articles/how-to-build-a-capsule-wardrobe/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | isDraft: false
3 | isMainHeadline: false
4 | isSubHeadline: true
5 | description: Streamline your wardrobe with timeless pieces that suit your style.
6 | title: How to Build a Capsule Wardrobe
7 | cover: '@assets/images/articles/how-to-build-a-capsule-wardrobe/cover.avif'
8 | category: wellness
9 | publishedTime: 2024-11-19T00:00:00.000Z
10 | authors:
11 | - chloe-nguyen
12 | ---
13 | A capsule wardrobe is about owning fewer clothes that offer endless outfit possibilities. Learn how to curate your perfect wardrobe.
14 |
15 | ## Why Choose a Capsule Wardrobe?
16 |
17 | Simplifying your wardrobe saves time, money, and reduces decision fatigue while ensuring you always feel confident in what you wear.
18 |
19 | ...
20 |
--------------------------------------------------------------------------------
/src/content/articles/javascript-math-is-a-lie/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | isDraft: false
3 | isMainHeadline: false
4 | isSubHeadline: false
5 | description: >-
6 | A hilariously painful journey through floating point math in JavaScript. Yes,
7 | 0.1 + 0.2 ≠ 0.3.
8 | title: JavaScript Math is a Lie
9 | cover: '@assets/images/articles/java-script-math-is-a-lie/cover.avif'
10 | category: programming
11 | publishedTime: 2025-04-15T04:43:00.000Z
12 | authors:
13 | - liam-leonard
14 | - jane-doe
15 | ---
16 | Ever tried `0.1 + 0.2 === 0.3` in JS? Yeah, it returns false. In this satirical-yet-informative piece, we dig into why JavaScript math feels like it’s gaslighting us and how to work around it without losing your mind.
17 |
--------------------------------------------------------------------------------
/src/content/articles/mastering-time-management/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | isDraft: false
3 | isMainHeadline: false
4 | isSubHeadline: true
5 | description: Learn practical strategies to manage your time and increase productivity.
6 | title: Mastering Time Management
7 | cover: '@assets/images/articles/mastering-time-management/cover.avif'
8 | category: productivity
9 | publishedTime: 2024-11-16T00:00:00.000Z
10 | authors:
11 | - liam-leonard
12 | ---
13 | Time is our most valuable resource, yet it often feels like there’s never enough of it. Mastering time management can help you achieve more with less stress.
14 |
15 | ## Why Time Management Matters
16 |
17 | Effective time management is key to balancing your personal and professional life, reducing stress, and achieving your goals.
18 |
19 | ...
20 |
--------------------------------------------------------------------------------
/src/content/articles/promises-in-javascript-trust-issues-in-code/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | isDraft: false
3 | isMainHeadline: false
4 | isSubHeadline: false
5 | description: ' An absurdly fun breakdown of promises, async/await, and why your code never waits when you want it to.'
6 | title: 'Promises in JavaScript: Trust Issues in Code'
7 | cover: '@assets/images/articles/promises-in-javascript-trust-issues-in-code/cover.avif'
8 | category: programming
9 | publishedTime: 2025-04-14T03:42:00.000Z
10 | authors:
11 | - john-smith
12 | ---
13 | Promises in JavaScript are like the friend who says they’ll help you move and shows up three hours late. In this article, we unpack promises, async/await, and how to stop chaining `.then()` like it’s a bad habit.
14 |
--------------------------------------------------------------------------------
/src/content/articles/the-art-of-minimalist-living/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | isDraft: false
3 | isMainHeadline: false
4 | isSubHeadline: true
5 | description: Discover how minimalism can bring clarity and purpose to your life.
6 | title: The Art of Minimalist Living
7 | cover: '@assets/images/articles/the-art-of-minimalist-living/cover.avif'
8 | category: lifestyle
9 | publishedTime: 2024-11-15T00:00:00.000Z
10 | authors:
11 | - maria-gonzalez
12 | ---
13 | Minimalism isn’t just about owning fewer things; it’s a mindset that emphasizes intentional living. Learn how to embrace minimalism and its benefits.
14 |
15 | ## What Is Minimalism?
16 |
17 | Minimalism is a lifestyle that encourages you to focus on what truly matters by eliminating excess.
18 |
19 | ...
20 |
--------------------------------------------------------------------------------
/src/content/articles/the-basics-of-personal-finance/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | isDraft: false
3 | isMainHeadline: false
4 | isSubHeadline: true
5 | description: >-
6 | Understand the fundamentals of budgeting, saving, and investing for a secure
7 | future.
8 | title: The Basics of Personal Finance
9 | cover: '@assets/images/articles/the-basics-of-personal-finance/cover.avif'
10 | category: finance
11 | publishedTime: 2024-11-17T00:00:00.000Z
12 | authors:
13 | - john-smith
14 | ---
15 | Personal finance is about more than just numbers; it’s about building habits that lead to financial freedom. Start your journey with these basics.
16 |
17 | ## Budgeting 101
18 |
19 | Creating and sticking to a budget is the foundation of financial health. Learn how to allocate your income wisely.
20 |
21 | ...
22 |
--------------------------------------------------------------------------------
/src/content/articles/the-benefits-of-meditation/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | isDraft: false
3 | isMainHeadline: false
4 | isSubHeadline: true
5 | description: Explore how meditation can enhance your mental and physical health.
6 | title: The Benefits of Meditation
7 | cover: '@assets/images/articles/the-benefits-of-meditation/cover.avif'
8 | category: health
9 | publishedTime: 2024-11-12T00:00:00.000Z
10 | authors:
11 | - sofia-martinez
12 | ---
13 | Meditation is a powerful tool for achieving mindfulness and reducing stress. This article delves into the science behind meditation and its practical benefits.
14 |
15 | ## Why Meditate?
16 |
17 | Meditation helps improve focus, reduce anxiety, and foster a sense of inner peace.
18 |
19 | ...
20 |
--------------------------------------------------------------------------------
/src/content/articles/top-5-web-development-frameworks-for-2025/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | isDraft: false
3 | isMainHeadline: false
4 | isSubHeadline: true
5 | description: A comparison of the most popular frameworks for modern web development.
6 | title: Top 5 Web Development Frameworks for 2025
7 | cover: '@assets/images/articles/top-5-web-development-frameworks-for-2025/cover.avif'
8 | category: technology
9 | publishedTime: 2024-11-13T00:00:00.000Z
10 | authors:
11 | - rajesh-patel
12 | ---
13 | Frameworks simplify the process of web development by providing pre-built tools and structures. Let’s compare React, Vue.js, Angular, Svelte, and Next.js.
14 |
15 | ## React
16 |
17 | React is a declarative, component-based JavaScript library for building user interfaces.
18 |
19 | ...
20 |
--------------------------------------------------------------------------------
/src/content/articles/understanding-java-script-closures/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | isDraft: false
3 | isMainHeadline: true
4 | isSubHeadline: false
5 | description: An in-depth look at closures in JavaScript and their applications.
6 | title: Understanding JavaScript Closures
7 | cover: '@assets/images/articles/understanding-java-script-closures/cover.avif'
8 | category: programming
9 | publishedTime: 2024-11-11T00:00:00.000Z
10 | authors:
11 | - ahmed-khan
12 | - jane-doe
13 | ---
14 | Closures are a fundamental concept in JavaScript that allow functions to access variables from their outer scope. This article explains closures, their practical uses, and common pitfalls.
15 |
16 | ## What Are Closures?
17 |
18 | A closure is the combination of a function bundled together with references to its surrounding state.
19 |
20 | ```js
21 | function outerFunction(outerVariable) {
22 | return function innerFunction(innerVariable) {
23 | console.log(`Outer Variable: ${outerVariable}`);
24 | console.log(`Inner Variable: ${innerVariable}`);
25 | };
26 | }
27 |
28 | const closureExample = outerFunction("I am from outerFunction");
29 |
30 | // Calling the inner function with the closure
31 | closureExample("I am from innerFunction");
32 |
33 | ```
34 |
--------------------------------------------------------------------------------
/src/content/articles/why-does-this-hate-me-demystifying-java-script-s-this/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | isDraft: false
3 | isMainHeadline: false
4 | isSubHeadline: false
5 | description: >-
6 | Why JavaScript's `this` keyword breaks brains and hearts—explained with
7 | relatable memes.
8 | title: Why Does this Hate Me? Demystifying JavaScripts this
9 | cover: >-
10 | @assets/images/articles/why-does-this-hate-me-demystifying-java-script-s-this/cover.avif
11 | category: programming
12 | publishedTime: 2025-04-15T10:36:00.000Z
13 | authors:
14 | - jane-doe
15 | - john-smith
16 | ---
17 | Ah yes, the infamous `this` keyword in JavaScript—responsible for more broken code and debugging tears than any other. In this article, we’ll laugh through the pain and explain what `this` actually means in different contexts.
18 |
--------------------------------------------------------------------------------
/src/content/authors/ahmed-khan/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | name: Ahmed Khan
3 | job: Political Correspondent
4 | avatar: '@assets/images/authors/ahmed-khan/avatar.jpg'
5 | bio: Ahmed provides in-depth analysis on political developments worldwide.
6 | social:
7 | - name: Twitter
8 | url: https://twitter.com
9 | icon: twitter-icon.svg
10 | ---
11 |
--------------------------------------------------------------------------------
/src/content/authors/chloe-nguyen/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | name: Chloe Nguyen
3 | job: Environmental Journalist
4 | avatar: '@assets/images/authors/chloe-nguyen/avatar.jpg'
5 | bio: Chloe reports on climate change, sustainability, and green innovations.
6 | social:
7 | - name: LinkedIn
8 | url: https://linkedin.com/in/chloenguyen
9 | icon: linkedin-icon.svg
10 | - name: Twitter
11 | url: https://twitter.com/chloenguyen
12 | icon: twitter-icon.svg
13 | ---
14 |
--------------------------------------------------------------------------------
/src/content/authors/emily-devis/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | name: Emily Davis
3 | job: Science Journalist
4 | avatar: '@assets/images/authors/emily-devis/avatar.jpg'
5 | bio: Emily writes about groundbreaking research in science and medicine.
6 | social:
7 | - name: LinkedIn
8 | url: https://linkedin.com/in/emilydavis
9 | icon: linkedin-icon.svg
10 | ---
11 |
--------------------------------------------------------------------------------
/src/content/authors/jane-doe/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | name: Jane Doe
3 | job: Investigative Journalist
4 | avatar: '@assets/images/authors/jane-doe/avatar.jpg'
5 | bio: >-
6 | Jane is an award-winning journalist specializing in investigative reporting on
7 | global issues.
8 | social:
9 | - name: Twitter
10 | url: https://twitter.com/janedoe
11 | icon: twitter-icon.svg
12 | - name: LinkedIn
13 | url: https://linkedin.com/in/janedoe
14 | icon: linkedin-icon.svg
15 | ---
16 |
--------------------------------------------------------------------------------
/src/content/authors/john-smith/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | name: John Smith
3 | job: Tech Writer
4 | avatar: '@assets/images/authors/john-smith/avatar.jpg'
5 | bio: John covers the latest advancements in technology and startups.
6 | social:
7 | - name: Twitter
8 | url: https://twitter.com/johnsmith
9 | icon: twitter-icon.svg
10 | - name: GitHub
11 | url: https://github.com/johnsmith
12 | icon: github-icon.svg
13 | ---
14 |
--------------------------------------------------------------------------------
/src/content/authors/liam-leonard/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | name: Liam Leonard
3 | job: Sports Journalist
4 | avatar: '@assets/images/authors/liam-leonard/avatar.jpg'
5 | bio: Liam covers major sporting events and stories behind the games.
6 | social:
7 | - name: Twitter
8 | url: https://twitter.com/liamsports
9 | icon: twitter-icon.svg
10 | ---
11 |
--------------------------------------------------------------------------------
/src/content/authors/maria-gonzalez/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | name: Maria Gonzalez
3 | job: Travel Blogger & Journalist
4 | avatar: '@assets/images/authors/maria-gonzalez/avatar.jpg'
5 | bio: Maria explores the world, sharing stories from remote corners and hidden gems.
6 | social:
7 | - name: Instagram
8 | url: https://instagram.com/mariagonzalez
9 | icon: instagram-icon.svg
10 | - name: Facebook
11 | url: https://facebook.com/mariagonzalez
12 | icon: facebook-icon.svg
13 | ---
14 |
--------------------------------------------------------------------------------
/src/content/authors/olivier-brown/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | name: Oliver Brown
3 | job: Tech and Gaming Reporter
4 | avatar: '@assets/images/authors/olivier-brown/avatar.jpg'
5 | bio: Oliver writes reviews and in-depth analyses of the gaming industry.
6 | social:
7 | - name: YouTube
8 | url: https://youtube.com/olivergaming
9 | icon: youtube-icon.svg
10 | - name: Twitter
11 | url: https://twitter.com/oliverbrown
12 | icon: twitter-icon.svg
13 | ---
14 |
--------------------------------------------------------------------------------
/src/content/authors/rajesh-patel/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | name: Rajesh Patel
3 | job: Business and Finance Reporter
4 | avatar: '@assets/images/authors/rajesh-patel/avatar.jpg'
5 | bio: Rajesh analyzes financial trends and business innovations.
6 | social:
7 | - name: Twitter
8 | url: https://twitter.com/rajeshpatel
9 | icon: twitter-icon.svg
10 | - name: LinkedIn
11 | url: https://linkedin.com/in/rajeshpatel
12 | icon: linkedin-icon.svg
13 | ---
14 |
--------------------------------------------------------------------------------
/src/content/authors/sofia-martinez/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | name: Sofia Martinez
3 | job: Arts and Culture Critic
4 | avatar: '@assets/images/authors/sofia-martinez/avatar.jpg'
5 | bio: Sofia critiques art, literature, and cultural events with a keen eye.
6 | social:
7 | - name: Instagram
8 | url: https://instagram.com/sofiaarts
9 | icon: instagram-icon.svg
10 | ---
11 |
--------------------------------------------------------------------------------
/src/content/categories/finance/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Finance",
3 | "path": "finance"
4 | }
--------------------------------------------------------------------------------
/src/content/categories/health/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Health",
3 | "path": "health"
4 | }
--------------------------------------------------------------------------------
/src/content/categories/lifestyle/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Lifestyle",
3 | "path": "lifestyle"
4 | }
--------------------------------------------------------------------------------
/src/content/categories/productivity/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Productivity",
3 | "path": "productivity"
4 | }
--------------------------------------------------------------------------------
/src/content/categories/programming/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Programming",
3 | "path": "programming"
4 | }
--------------------------------------------------------------------------------
/src/content/categories/technology/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Technology"
3 | ,"path": "technology"
4 | }
--------------------------------------------------------------------------------
/src/content/categories/wellness/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Wellness",
3 | "path": "wellness"
4 | }
--------------------------------------------------------------------------------
/src/content/views/about.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: "About"
3 | description: "Learn about the author, his background, and his interests."
4 | blocks:
5 | - name: "ABOUT"
6 | title: "About us"
7 | ---
8 | import screenshot from '../../assets/images/screenshot-astronews.png';
9 | import performances from '../../assets/images/Performances.png';
10 | import { Image } from 'astro:assets';
11 |
12 |
13 | # Astro News 📰
14 |
15 | A news website built with Astro, designed to provide a modern and responsive news reading experience.
16 |
17 | > **ℹ️ Info**
18 | > Astro news is my first open-source project, so there may be some bugs or issues. I’ll address them as quickly as possible.
19 |
20 |
21 |
22 |
23 | ## 🌐 Demo
24 |
25 | Explore the live demo here: [**Live Demo**](https://astro-news-six.vercel.app/)
26 |
27 | ## 🚀 Installation
28 |
29 | Follow the steps below to set up and run the project locally:
30 |
31 | ### Clone the Repository
32 |
33 | ```bash
34 | git clone https://github.com/Mrahmani71/astro-news.git
35 | ```
36 |
37 | ### Install Dependencies
38 |
39 | ``` bash
40 | bun install
41 | ```
42 |
43 | ### Run Development Server
44 |
45 | ```bash
46 | bun dev
47 | ```
48 |
49 | ### Running Keystatic CMS
50 |
51 | 1. Rename `.env.example` to `.env`.
52 |
53 | 2. Set `RUN_KEYSTATIC=true`.
54 |
55 | 3. Start the development server:
56 |
57 | ```bash
58 | bun dev
59 | ```
60 |
61 | 4. Open `http://localhost:4321/keystatic` in your browser.
62 |
63 | ## ✨ Features
64 |
65 | ### Implemented Features
66 |
67 | - Content Layer
68 | - Keystatic CMS
69 | - Navigation
70 | - Responsive Design
71 | - Pagination
72 | - Search Functionality
73 | - RSS Feed
74 | - Sitemap
75 | - Dark Mode
76 | - SEO Optimization (~)
77 |
78 | ### Upcoming Features
79 |
80 | - Open Graph (OG) Image Generation
81 |
82 | ## 💻 Technologies
83 |
84 | This project leverages cutting-edge web technologies:
85 |
86 | - [Astro V5.7](https://astro.build) - Modern static site builder
87 | - [KeyStatic](https://keystatic.com) - Headless content-management system
88 | - [Tailwind CSS](https://tailwindcss.com) - Utility-first CSS framework
89 | - [DaisyUI](https://daisyui.com/) - Tailwind CSS component library
90 | - [TypeScript](https://typescriptlang.org) - Typed JavaScript
91 | - [MDX](https://mdxjs.com) - Markdown with JSX support
92 | - [Bun V1.2.10](https://bun.sh) - Fast JavaScript runtime
93 | - [Vercel](https://vercel.com) - Deployment platform
94 | - [HugeIcons](https://hugeicons.com) - Icon library
95 |
96 | ## 💡 Inspirations and Code Concepts
97 |
98 | This project draws inspiration from the following sources:
99 |
100 | ### Designs
101 |
102 | - [BBC News](https://www.bbc.com)
103 | - [NewsHub - News Website](https://dribbble.com/shots/21678041-NewsHub-News-Website)
104 | - [Let'sread - News Landing Page](https://dribbble.com/shots/24675325-Let-sread-News-Landing-Page)
105 |
106 | ### Articles
107 |
108 | - [Creating A Pagination Component With Astro](https://rimdev.io/creating-a-pagination-component-with-astro)
109 | - [Adding search to static Astro sites](https://website-thomas-astro.vercel.app/blog/search-static-astro-website)
110 |
111 | ## 📄 License
112 |
113 | Open sourced under the [MIT license](https://github.com/Mrahmani71/astro-news/blob/master/LICENSE.md).
114 |
115 | ## 🤝 Contributing
116 |
117 | Contributions, issues, and feature requests are welcome! Feel free to check the [issues page](https://github.com/Mrahmani71/astro-news/issues).
--------------------------------------------------------------------------------
/src/content/views/articles.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Latest articles"
3 | description: "Leatest articles published in the Astro News."
4 | blocks:
5 | - name: "HEADER"
6 | title: "Latest Articles"
7 | ---
--------------------------------------------------------------------------------
/src/content/views/author.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: "articles"
3 | description: "List of articles by author in the Astro News."
4 | blocks:
5 | - name: "HEADER"
6 | title: "articles"
7 | description: "List of authors of the Astro News."
8 | - name: "ATTENTION"
9 | description: "These names and images are not real. The images are from Unsplash, used in this project to show how to add authors to articles."
10 | ---
--------------------------------------------------------------------------------
/src/content/views/authors.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Authors"
3 | description: "Learn about the authors behind the Astro News."
4 | blocks:
5 | - name: "HEADER"
6 | title: "List of Authors"
7 | description: "List of authors of the Astro News."
8 | - name: "ATTENTION"
9 | description: "These names and images are not real. The images are from Unsplash, used in this project to show how to add authors to articles."
10 | ---
--------------------------------------------------------------------------------
/src/content/views/categories.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Categories"
3 | description: "Learn about the categories of articles in the Astro News."
4 | blocks:
5 | - name: "HEADER"
6 | title: "List of Categories"
7 | ---
--------------------------------------------------------------------------------
/src/content/views/contact.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Contact"
3 | description: "Contact us for any inquiries or feedback."
4 | blocks:
5 | - name: "CONTACT"
6 | title: "Contact Us"
7 | ---
--------------------------------------------------------------------------------
/src/content/views/error404.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: "404 - Page Not Found"
3 | description: "Page not found."
4 | blocks:
5 | - name: "HERO"
6 | title: "404 - Page Not Found"
7 | description: "Oops! The page you're looking for doesn't exist."
8 | link_text: "Return to Homepage"
9 | link_url: "/"
10 | ---
--------------------------------------------------------------------------------
/src/content/views/home.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Astro News"
3 | description: "Welcome to the Astro News, a newsletter about Astro."
4 | blocks:
5 | - name: "HEADER"
6 | title: "Welcome to the Astro News"
7 | ---
--------------------------------------------------------------------------------
/src/content/views/search.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Search"
3 | description: "Search articles in the Astro News."
4 | blocks:
5 | - name: "HEADER"
6 | title: "Search Articles"
7 | ---
--------------------------------------------------------------------------------
/src/layouts/base.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { SITE } from "@/lib/config";
3 | import Head from "@/components/bases/head.astro";
4 | import Header from "@/components/shared/header.astro";
5 | import Footer from "@/components/shared/footer.astro";
6 | import type { Entry } from "@/lib/types";
7 | import { getMeta } from "@/lib/utils/getMeta";
8 |
9 | type Props = {
10 | entry: Entry;
11 | };
12 |
13 | const { entry } = Astro.props;
14 |
15 | const meta = await getMeta(entry);
16 | ---
17 |
18 |
19 |
20 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/layouts/content.astro:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/layouts/list.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { CollectionEntry } from "astro:content";
3 | import BaseLayout from "./base.astro";
4 | import ViewListHeader from "@/components/shared/view-list-header.astro";
5 |
6 | type Props = {
7 | header: string;
8 | entry: CollectionEntry<"views">;
9 | };
10 |
11 | const { entry, header } = Astro.props;
12 | ---
13 |
14 |
15 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/lib/config/index.ts:
--------------------------------------------------------------------------------
1 | import type { Link } from "../types";
2 |
3 | export const SITE = {
4 | title: "Astro News",
5 | description: "A news website built with Astro",
6 | author: "Mohammad Rahmani",
7 | url: "https://astro-news-six.vercel.app",
8 | github: "https://github.com/Mrahmani71/astro-news",
9 | locale: "en-US",
10 | dir: "ltr",
11 | charset: "UTF-8",
12 | basePath: "/",
13 | postsPerPage: 4,
14 | };
15 |
16 | export const NAVIGATION_LINKS: Link[] = [
17 | {
18 | href: "/categories/technology",
19 | text: "Technology",
20 | },
21 | {
22 | href: "/categories/programming",
23 | text: "Programming",
24 | },
25 | {
26 | href: "/categories/lifestyle",
27 | text: "Lifestyle",
28 | },
29 | {
30 | href: "/categories/productivity",
31 | text: "Productivity",
32 | },
33 | {
34 | href: "/categories/health",
35 | text: "Health",
36 | },
37 | {
38 | href: "/categories/finance",
39 | text: "Finance",
40 | },
41 | ];
42 |
43 | export const OTHER_LINKS: Link[] = [
44 | {
45 | href: "/about",
46 | text: "About us",
47 | },
48 | {
49 | href: "/authors",
50 | text: "Authors",
51 | },
52 | {
53 | href: "/contact",
54 | text: "Contact",
55 | },
56 | {
57 | href: "/privacy",
58 | text: "Privacy",
59 | },
60 | {
61 | href: "/terms",
62 | text: "Terms",
63 | },
64 | {
65 | href: "/cookie-policy",
66 | text: "Cookie Policy",
67 | },
68 | {
69 | href: "https://astro-news-six.vercel.app/rss.xml",
70 | text: "RSS",
71 | },
72 | {
73 | href: "https://astro-news-six.vercel.app/sitemap-index.xml",
74 | text: "Sitemap",
75 | },
76 | ];
77 |
78 | export const SOCIAL_LINKS: Link[] = [
79 | {
80 | href: "https://github.com",
81 | text: "GitHub",
82 | icon: "github",
83 | },
84 | {
85 | href: "httpe://www.t.me",
86 | text: "Telegram",
87 | icon: "telegram",
88 | },
89 | {
90 | href: "https://twitter.com",
91 | text: "Twitter",
92 | icon: "newTwitter",
93 | },
94 | {
95 | href: "https://www.facebook.com",
96 | text: "Facebook",
97 | icon: "facebook",
98 | },
99 | ];
100 |
--------------------------------------------------------------------------------
/src/lib/handlers/articles.ts:
--------------------------------------------------------------------------------
1 | import { getCollection } from "astro:content";
2 |
3 | const articlesCollection = (
4 | await getCollection("articles", ({ data }) => {
5 | return data.isDraft !== true && new Date(data.publishedTime) < new Date();
6 | })
7 | ).sort((a, b) =>
8 | new Date(b.data.publishedTime)
9 | .toISOString()
10 | .localeCompare(new Date(a.data.publishedTime).toISOString())
11 | );
12 |
13 | export const articlesHandler = {
14 | allArticles: () => articlesCollection,
15 |
16 | mainHeadline: () => {
17 | const article = articlesCollection.filter(
18 | (article) => article.data.isMainHeadline === true
19 | )[0];
20 | if (!article)
21 | throw new Error(
22 | "Please ensure there is at least one item to display for the main headline."
23 | );
24 | return article;
25 | },
26 |
27 | subHeadlines: () => {
28 | const mainHeadline = articlesHandler.mainHeadline();
29 | const subHeadlines = articlesCollection
30 | .filter(
31 | (article) =>
32 | article.data.isSubHeadline === true &&
33 | mainHeadline.id !== article.id
34 | )
35 | .slice(0, 4);
36 |
37 | if (subHeadlines.length === 0)
38 | throw new Error(
39 | "Please ensure there is at least one item to display for the sub headlines."
40 | );
41 | return subHeadlines;
42 | },
43 | };
44 |
--------------------------------------------------------------------------------
/src/lib/handlers/authors.ts:
--------------------------------------------------------------------------------
1 | import { getCollection } from "astro:content";
2 |
3 | const authorsCollection = await getCollection("authors");
4 |
5 | export const authorsHandler = {
6 | allAuthors: () => authorsCollection,
7 | limitAurhors: (limit: number) => authorsCollection.slice(0, limit),
8 | getAuthors: (authors: { collection: string; id: string }[]) => {
9 | return authors.map(({ id }) => {
10 | const author = authorsCollection.find((author) => author.id === id);
11 | if (!author) {
12 | throw new Error(`Author ${id} not found`);
13 | }
14 | return author;
15 | });
16 | },
17 | findAuthor: (id: string) => {
18 | const author = authorsCollection.find((author) => author.id === id);
19 | if (!author) {
20 | throw new Error(`Author ${id} not found`);
21 | }
22 | return author;
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/src/lib/handlers/categories.ts:
--------------------------------------------------------------------------------
1 | import { getCollection } from "astro:content";
2 | import { articlesHandler } from "./articles";
3 |
4 | const categoriesCollection = await getCollection('categories');
5 |
6 | export const categoriesHandler = {
7 | allCategories: () => categoriesCollection.sort((a, b) => a.data.title.localeCompare(b.data.title)),
8 | oneCategory: (categoryId: string) => {
9 | const category = categoriesCollection.find((category) => category.id === categoryId);
10 | if (!category) {
11 | throw new Error(`Category with id ${categoryId} not found`);
12 | }
13 | return category;
14 | },
15 | allCategoriesWithLatestArticles: () => {
16 | return categoriesCollection.map((category) => {
17 | const articles = articlesHandler.allArticles()
18 | .filter((article) => article.data.category.id === category.id);
19 | return {
20 | ...category,
21 | data: {
22 | ...category.data,
23 | count: articles.length,
24 | latestArticles: articles.slice(0, 3)
25 | }
26 | }
27 | })
28 | }
29 | }
--------------------------------------------------------------------------------
/src/lib/keystatic/articlesKs.ts:
--------------------------------------------------------------------------------
1 | import { collection, fields } from "@keystatic/core";
2 |
3 | export const articlesKs = collection({
4 | label: "Articles",
5 | slugField: "title",
6 | path: "src/content/articles/*/",
7 | format: { contentField: "content" },
8 | entryLayout: "form",
9 | schema: {
10 | isDraft: fields.checkbox({
11 | label: "Is this a draft?",
12 | defaultValue: false,
13 | }),
14 | isMainHeadline: fields.checkbox({
15 | label: "Is this a main headline?",
16 | defaultValue: false,
17 | }),
18 | isSubHeadline: fields.checkbox({
19 | label: "Is this a sub headline?",
20 | defaultValue: false,
21 | }),
22 | description: fields.text({
23 | label: "Description",
24 | validation: { isRequired: true, length: { max: 160 } },
25 | }),
26 | title: fields.slug({
27 | name: { label: "Title", validation: { length: { max: 60 } } },
28 | }),
29 | cover: fields.image({
30 | label: "Cover",
31 | directory: "src/assets/images/articles",
32 | publicPath: "@assets/images/articles/",
33 | }),
34 | category: fields.relationship({
35 | label: "Category",
36 | collection: "categories",
37 | }),
38 | publishedTime: fields.datetime({
39 | label: "Published Time",
40 | validation: { isRequired: true },
41 | }),
42 | authors: fields.array(
43 | fields.relationship({
44 | label: "Authors",
45 | collection: "authors",
46 | }),
47 | {
48 | label: "Authors",
49 | itemLabel: (props) => props.value ?? "",
50 | validation: {
51 | length: {
52 | min: 1,
53 | },
54 | },
55 | }
56 | ),
57 | content: fields.mdx({
58 | label: "Content",
59 | options: {
60 | image: {
61 | directory: "src/assets/images/articles",
62 | publicPath: "@assets/images/articles",
63 | },
64 | },
65 | }),
66 | },
67 | });
68 |
--------------------------------------------------------------------------------
/src/lib/keystatic/authorsKs.ts:
--------------------------------------------------------------------------------
1 | import { collection, fields } from "@keystatic/core";
2 |
3 | export const authorsKs = collection({
4 | label: "Authors",
5 | slugField: "name",
6 | path: "src/content/authors/*/",
7 | format: { contentField: "content" },
8 | entryLayout: "form",
9 | schema: {
10 | name: fields.slug({ name: { label: "Name" } }),
11 | job: fields.text({ label: "Job" }),
12 | avatar: fields.image({
13 | label: "Avatar",
14 | directory: "src/assets/images/authors",
15 | publicPath: "@assets/images/authors",
16 | }),
17 | bio: fields.text({ label: "Bio" }),
18 | social: fields.array(
19 | fields.object({
20 | name: fields.text({ label: "Name", validation: { isRequired: true } }),
21 | url: fields.url({ label: "URL", validation: { isRequired: true } }),
22 | icon: fields.text({ label: "Icon", validation: { isRequired: true } }),
23 | }),
24 | {
25 | label: "Social Links",
26 | itemLabel: (props) => props.fields?.name.value ?? "",
27 | }
28 | ),
29 | content: fields.mdx({
30 | label: "Content",
31 | options: {
32 | image: {
33 | directory: "src/assets/images/authors",
34 | publicPath: "@assets/images/authors",
35 | },
36 | },
37 | }),
38 | },
39 | });
40 |
--------------------------------------------------------------------------------
/src/lib/keystatic/categoriesKs.ts:
--------------------------------------------------------------------------------
1 | import { collection, fields } from "@keystatic/core";
2 |
3 | export const categoriesKs = collection({
4 | label: "Categories",
5 | slugField: "path",
6 | path: "src/content/categories/*/",
7 | format: { data: "json" },
8 | schema: {
9 | title: fields.text({
10 | label: "Title",
11 | description: "The title of the category.",
12 | }),
13 | path: fields.text({
14 | label: "Path",
15 | description: "The path of the category.",
16 | }),
17 | },
18 | });
19 |
--------------------------------------------------------------------------------
/src/lib/keystatic/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./articlesKs";
2 | export * from "./authorsKs";
3 | export * from "./categoriesKs";
4 |
--------------------------------------------------------------------------------
/src/lib/schema/index.ts:
--------------------------------------------------------------------------------
1 | import { reference, z } from "astro:content";
2 | import type { ImageFunction } from "astro:content";
3 |
4 | export const articleSchema = (image: ImageFunction) =>
5 | z.object({
6 | isDraft: z.boolean().default(false),
7 | isMainHeadline: z.boolean().default(false),
8 | isSubHeadline: z.boolean().default(false),
9 | cover: image(),
10 | covert_alt: z.string().optional(),
11 | title: z.string().max(60, "Too long, max 60 characters"),
12 | description: z.string().max(160, "Too long, max 160 characters"),
13 | category: reference("categories"),
14 | authors: z.array(reference("authors")).min(1),
15 | publishedTime: z.string().datetime().or(z.date()),
16 | });
17 |
18 | export const viewSchema = z.object({
19 | title: z.string(),
20 | description: z.string(),
21 | blocks: z.array(z.any()),
22 | });
23 |
24 | export const categorySchema = z.object({
25 | title: z.string(),
26 | path: z
27 | .string()
28 | .regex(
29 | /^[a-z0-9]+(?:-[a-z0-9]+)*$/,
30 | "The string must be a slug (only lowercase letters, numbers, and hyphens)."
31 | ),
32 | });
33 |
34 | export const authorSchema = (Image: ImageFunction) =>
35 | z.object({
36 | name: z.string(),
37 | job: z.string(),
38 | avatar: Image(),
39 | bio: z.string(),
40 | social: z.array(
41 | z.object({
42 | name: z.string(),
43 | url: z.string(),
44 | icon: z.string(),
45 | })
46 | ),
47 | });
48 |
49 | // avatar: Image().refine(
50 | // (img) => {
51 | // const isValidWidth = img.width > 100 && img.width < 2000;
52 | // const isValidHeight = img.height > 100 && img.height < 2000;
53 | // return isValidWidth && isValidHeight;
54 | // },
55 | // "Avatar image must have width and height between 100 and 2000"
56 | // ),
57 |
--------------------------------------------------------------------------------
/src/lib/types/index.ts:
--------------------------------------------------------------------------------
1 | import type { CollectionEntry } from "astro:content";
2 |
3 | export type Icon = {
4 | size?: string;
5 | width?: string;
6 | height?: string;
7 | color?: string;
8 | strokeWidth?: string;
9 | };
10 |
11 | export type Link = {
12 | href: string;
13 | text: string;
14 | icon?: string;
15 | target?: "_blank" | "_self";
16 | };
17 |
18 | type Author = {
19 | name: string;
20 | link: string;
21 | };
22 |
23 | export type Meta = {
24 | title: string;
25 | metaTitle: string;
26 | description: string;
27 | type: "article" | "website";
28 | ogImage: string;
29 | ogImageAlt: string;
30 | };
31 |
32 | export type ArticleMeta = Meta & {
33 | publishedTime: string;
34 | lastModified: string;
35 | authors: Author[];
36 | };
37 |
38 | export type Entry = CollectionEntry<"articles" | "views">;
39 |
--------------------------------------------------------------------------------
/src/lib/utils/date.ts:
--------------------------------------------------------------------------------
1 | import { formatDistanceToNow, parseISO, format } from "date-fns";
2 |
3 | const FORMAT_LONG = "EEEE, MMMM d, yyyy h:mm a zz";
4 | const FORMAT_SHORT = "MMMM dd, yyyy zz";
5 |
6 | const dateCache = new Map();
7 |
8 |
9 | export const getDateDistance = (date: string) =>
10 | formatDistanceToNow(parseISO(date), {
11 | addSuffix: true,
12 | });
13 |
14 |
15 | export const normalizeDate = (date: string | Date): string =>
16 | date instanceof Date ? date.toISOString() : date;
17 |
18 | const getParsedDate = (dateString: string): Date => {
19 | if (dateCache.has(dateString)) {
20 | return dateCache.get(dateString)!;
21 | }
22 |
23 | const parsedDate = parseISO(dateString);
24 |
25 | if (Number.isNaN(parsedDate.getTime())) {
26 | throw new Error("Invalid date value provided.");
27 | }
28 |
29 | dateCache.set(dateString, parsedDate);
30 | return parsedDate;
31 | };
32 |
33 | export const formatDate = (
34 | date: string | Date,
35 | formatType: "long" | "short" = "long"
36 | ) => {
37 | // Ensure that the date is a valid Date string
38 | const dateString = date instanceof Date ? date.toISOString() : date;
39 |
40 | // Get parsed date from cache or parse it
41 | const parsedDate = getParsedDate(dateString);
42 |
43 | // Format the date based on the requested format
44 | return format(parsedDate, formatType === "short" ? FORMAT_SHORT : FORMAT_LONG);
45 | };
--------------------------------------------------------------------------------
/src/lib/utils/getMeta.ts:
--------------------------------------------------------------------------------
1 | import { render, type CollectionEntry } from "astro:content";
2 | import { authorsHandler } from "@/lib/handlers/authors";
3 | import { SITE } from "@/lib/config";
4 | import defaultImage from "@/assets/images/default-image.jpg";
5 | import type { ArticleMeta, Meta } from "@/lib/types";
6 | import { capitalizeFirstLetter } from "@/lib/utils/letter";
7 | import { normalizeDate } from "@/lib/utils/date";
8 |
9 | type GetMetaCollection = CollectionEntry<"articles" | "views">;
10 |
11 | const renderCache = new Map();
12 |
13 | export const getMeta = async (
14 | collection: GetMetaCollection,
15 | category?: string
16 | ): Promise => {
17 | try {
18 | const collectionId = `${collection.collection}-${collection.id}`;
19 |
20 | if (collection.collection === "articles") {
21 |
22 | if (renderCache.has(collectionId)) {
23 | return renderCache.get(collectionId);
24 | }
25 |
26 | const { remarkPluginFrontmatter } = await render(collection);
27 | const authors = authorsHandler.getAuthors(collection.data.authors);
28 |
29 | const meta: ArticleMeta = {
30 | title: `${capitalizeFirstLetter(collection.data.title)} - ${SITE.title}`,
31 | metaTitle: capitalizeFirstLetter(collection.data.title),
32 | description: collection.data.description,
33 | ogImage: collection.data.cover.src,
34 | ogImageAlt: collection.data.covert_alt || collection.data.title,
35 | publishedTime: normalizeDate(collection.data.publishedTime),
36 | lastModified: remarkPluginFrontmatter.lastModified,
37 | authors: authors.map((author) => ({
38 | name: author.data.name,
39 | link: `${author.id}`,
40 | })),
41 | type: "article",
42 | }
43 |
44 | renderCache.set(collectionId, meta);
45 |
46 | return meta;
47 | }
48 |
49 | if (collection.collection === "views") {
50 |
51 | const cacheKey = category ? `${collectionId}-${category}` : collectionId;
52 | if (renderCache.has(cacheKey)) {
53 | return renderCache.get(cacheKey);
54 | }
55 |
56 | const title = collection.id === "categories" && category
57 | ? `${capitalizeFirstLetter(category)} - ${SITE.title}`
58 | : collection.id === "home"
59 | ? SITE.title
60 | : `${capitalizeFirstLetter(collection.data.title)} - ${SITE.title}`;
61 |
62 | const meta: Meta = {
63 | title,
64 | metaTitle: capitalizeFirstLetter(collection.data.title),
65 | description: collection.data.description,
66 | ogImage: defaultImage.src,
67 | ogImageAlt: SITE.title,
68 | type: "website",
69 | };
70 | renderCache.set(cacheKey, meta);
71 | return meta;
72 | }
73 |
74 | throw new Error(`Invalid collection type: ${(collection as GetMetaCollection).collection}`);
75 | } catch (error) {
76 | console.error(`Error generating metadata for ${collection.id}:`, error);
77 | throw error;
78 | }
79 | };
80 |
--------------------------------------------------------------------------------
/src/lib/utils/letter.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Capitalizes the first letter of a string
3 | * @param val - The string to capitalize
4 | * @returns The string with its first letter capitalized
5 | */
6 | export const capitalizeFirstLetter = (val: string): string => {
7 | if (!val || typeof val !== 'string' || val.length === 0) {
8 | return val;
9 | }
10 |
11 | // Convert to string only once and cache the result
12 | const str = String(val);
13 | return str.charAt(0).toUpperCase() + str.slice(1);
14 | };
--------------------------------------------------------------------------------
/src/lib/utils/remarks.mjs:
--------------------------------------------------------------------------------
1 | import { execSync } from "node:child_process";
2 | import { statSync } from "node:fs";
3 | import getReadingTime from "reading-time";
4 | import { toString as ConvertToString } from "mdast-util-to-string";
5 |
6 | export function modifiedTime() {
7 | return (_, file) => {
8 | const filepath = file.history[0];
9 | const result = execSync(`git log -1 --pretty="format:%cI" "${filepath}"`);
10 | if (result.toString().length > 0 || result.toString() === "") {
11 | const result = statSync(filepath);
12 | file.data.astro.frontmatter.lastModified = result.mtime.toISOString();
13 | } else {
14 | file.data.astro.frontmatter.lastModified = result.toString();
15 | }
16 | };
17 | }
18 | export function readingTime() {
19 | return (tree, { data }) => {
20 | const textOnPage = ConvertToString(tree);
21 | const readingTime = getReadingTime(textOnPage, { wordsPerMinute: 180 });
22 | // readingTime.text will give us minutes read as a friendly string,
23 | // i.e. "3 min read"
24 | data.astro.frontmatter.minutesRead = readingTime.text;
25 | };
26 | }
27 |
--------------------------------------------------------------------------------
/src/pages/404.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getEntry } from "astro:content";
3 | import BaseLayout from "../layouts/base.astro";
4 |
5 | const entry = await getEntry("views", "error404");
6 |
7 | if (!entry) {
8 | throw new Error("404 page not found");
9 | }
10 |
11 | const [HERO] = entry.data.blocks;
12 | ---
13 |
14 |
15 |
22 |
23 |
--------------------------------------------------------------------------------
/src/pages/_home/authors.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import AuthorCard from "@/components/cards/authorCard.astro";
3 | import HeaderSection from "./headerSection.astro";
4 | import { authorsHandler } from "@/lib/handlers/authors";
5 |
6 | const authors = authorsHandler.limitAurhors(6);
7 | ---
8 |
9 |
10 |
11 |
15 | {authors.map((author) => )}
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/pages/_home/headerSection.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import ArrowRight02 from "@/assets/svgs/arrow-right-02.astro";
3 |
4 | type Props = {
5 | title: string;
6 | link_title: string;
7 | link_url: string;
8 | };
9 |
10 | const { title, link_title, link_url } = Astro.props;
11 | ---
12 |
13 |
26 |
--------------------------------------------------------------------------------
/src/pages/_home/headlines.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import MainHeadline from "@/components/cards/mainHeadline.astro";
3 | import SubHeadlineCard from "@/components/cards/subHeadlineCard.astro";
4 | import { articlesHandler } from "@/lib/handlers/articles";
5 |
6 | const mainHeadlineArticle = articlesHandler.mainHeadline();
7 | const subHeadlinesArticles = articlesHandler.subHeadlines();
8 | ---
9 |
10 |
14 |
15 |
16 |
17 |
18 | {
19 | subHeadlinesArticles.map((article, index) => (
20 |
25 | ))
26 | }
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/pages/_home/latestNews.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import NewsCard from "@/components/cards/newsCard.astro";
3 | import HeaderSection from "./headerSection.astro";
4 | import { articlesHandler } from "@/lib/handlers/articles";
5 |
6 | const articles = articlesHandler.allArticles();
7 | ---
8 |
9 |
10 |
15 |
16 | {
17 | articles
18 | .slice(0, 6)
19 | .map((article, index) => )
20 | }
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/pages/_home/news-ticker.astro:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Breaking News
5 | News
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/pages/about.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import ContentLayout from "@/layouts/content.astro";
3 | import BaseLayout from "@/layouts/base.astro";
4 | import { getEntry, render } from "astro:content";
5 |
6 | const entry = await getEntry("views", "about");
7 |
8 | if (!entry) {
9 | return Astro.redirect("/404");
10 | }
11 |
12 | const { Content } = await render(entry);
13 | ---
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/pages/articles/[id].astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { render } from "astro:content";
3 | import BaseLayout from "@/layouts/base.astro";
4 | import ContentLayout from "@/layouts/content.astro";
5 | import ArticleHeader from "./_components/article-header.astro";
6 |
7 | import { articlesHandler } from "@/lib/handlers/articles";
8 |
9 | export const getStaticPaths = async () => {
10 | const articles = articlesHandler.allArticles();
11 |
12 | return articles.map((article) => ({
13 | params: { id: article.id },
14 | props: { article },
15 | }));
16 | };
17 | const { article } = Astro.props;
18 |
19 | const { Content, remarkPluginFrontmatter } = await render(article);
20 | ---
21 |
22 |
23 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/pages/articles/[page].astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { GetStaticPaths } from "astro";
3 | import ListLayout from "@/layouts/list.astro";
4 | import { SITE } from "@/lib/config";
5 | import Pagination from "@/components/shared/pagination.astro";
6 | import { getEntry } from "astro:content";
7 | import WideCard from "@/components/cards/wideCard.astro";
8 |
9 | import { articlesHandler } from "@/lib/handlers/articles";
10 |
11 | export const getStaticPaths = (async ({ paginate }) => {
12 | const articles = articlesHandler.allArticles();
13 | return paginate(articles, { pageSize: SITE.postsPerPage });
14 | }) satisfies GetStaticPaths;
15 |
16 | const { page } = Astro.props;
17 | const articles = page.data;
18 | const pathname = new URL(Astro.request.url).pathname.split("/");
19 | const basePath = pathname[1];
20 |
21 | const entry = await getEntry("views", "articles");
22 |
23 | if (!entry) {
24 | return Astro.redirect("/404");
25 | }
26 |
27 | const [HEADER] = entry.data.blocks;
28 | ---
29 |
30 |
31 |
32 |
33 | {
34 | articles.map((article) => (
35 |
39 | ))
40 | }
41 |
42 |
43 | {
44 | (
45 |
54 | )
55 | }
56 |
57 |
--------------------------------------------------------------------------------
/src/pages/articles/_components/article-header.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { Image, Picture } from "astro:assets";
3 | import type { CollectionEntry } from "astro:content";
4 | import { formatDate, normalizeDate } from "@/lib/utils/date";
5 | import ResourcesAdd from "@/assets/svgs/resources-add.astro";
6 | import Time04 from "@/assets/svgs/time-04.astro";
7 | import Calendar04 from "@/assets/svgs/calendar-04.astro";
8 | import Divider from "@/components/bases/divider.astro";
9 | import { categoriesHandler } from "@/lib/handlers/categories";
10 | import { authorsHandler } from "@/lib/handlers/authors";
11 | import Share from "@/components/elements/share.astro";
12 |
13 | type Props = {
14 | article: CollectionEntry<"articles">;
15 | readingTime: string;
16 | };
17 |
18 | const { article, readingTime } = Astro.props;
19 |
20 | const category = categoriesHandler.oneCategory(article.data.category.id);
21 | const authors = authorsHandler.getAuthors(article.data.authors);
22 | ---
23 |
24 |
27 |
39 |
45 |
46 |
47 | {article.data.title}
48 |
49 |
50 |
51 |
52 |
53 | {category.data.title}
56 |
57 |
58 |
59 |
60 |
67 |
74 |
75 |
76 |
77 |
78 | {readingTime}
79 |
80 |
81 |
82 |
83 |
84 | {
85 | authors.map((author) => (
86 |
107 | ))
108 | }
109 |
110 |
111 |
112 |
113 |
114 |
115 |
--------------------------------------------------------------------------------
/src/pages/articles/index.astro:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/pages/authors/[id]/[page].astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { GetStaticPaths } from "astro";
3 | import { SITE } from "@/lib/config";
4 | import { articlesHandler } from "@/lib/handlers/articles";
5 | import { authorsHandler } from "@/lib/handlers/authors";
6 | import ListLayout from "@/layouts/list.astro";
7 | import WideCard from "@/components/cards/wideCard.astro";
8 | import Pagination from "@/components/shared/pagination.astro";
9 | import { getEntry } from "astro:content";
10 |
11 | export const getStaticPaths = (({ paginate }) => {
12 | const allAuthors = authorsHandler.allAuthors();
13 | const allArticles = articlesHandler.allArticles();
14 |
15 | return allAuthors.flatMap((author) => {
16 | const filteredArticles = allArticles.filter((article) =>
17 | article.data.authors.map((a) => a.id).includes(author.id),
18 | );
19 | return paginate(filteredArticles, {
20 | params: { id: author.id },
21 | pageSize: SITE.postsPerPage,
22 | });
23 | });
24 | }) satisfies GetStaticPaths;
25 |
26 | const { page } = Astro.props;
27 | const params = Astro.params;
28 | const articles = page.data;
29 | const pathname = new URL(Astro.request.url).pathname.split("/");
30 | const basePath = `${pathname[1]}/${pathname[2]}`;
31 |
32 | const entry = await getEntry("views", "author");
33 | const author = authorsHandler.findAuthor(params.id);
34 |
35 | if (!entry) {
36 | return Astro.redirect("/404");
37 | }
38 |
39 | const [HEADER] = entry.data.blocks;
40 | ---
41 |
42 |
52 |
53 |
54 | {
55 | articles.map((article) => (
56 |
60 | ))
61 | }
62 |
63 |
64 |
65 |
74 |
75 |
--------------------------------------------------------------------------------
/src/pages/authors/[id]/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { authorsHandler } from "@/lib/handlers/authors";
3 |
4 | export const getStaticPaths = async () => {
5 | const authors = authorsHandler.allAuthors();
6 |
7 | return authors.map((author) => ({
8 | params: { id: author.id },
9 | props: { author },
10 | }));
11 | };
12 |
13 | const { author } = Astro.props;
14 | ---
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/pages/authors/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getEntry } from "astro:content";
3 | import Info from "@/assets/svgs/info.astro";
4 | import AuthorCard from "@/components/cards/authorCard.astro";
5 | import ListLayout from "@/layouts/list.astro";
6 | import { authorsHandler } from "@/lib/handlers/authors";
7 |
8 | const entry = await getEntry("views", "authors");
9 |
10 | if (!entry) {
11 | return Astro.redirect("/404");
12 | }
13 |
14 | const authors = authorsHandler.allAuthors();
15 |
16 | const [HEADER, ATTENTION] = entry.data.blocks;
17 | ---
18 |
19 |
20 |
21 |
22 | {ATTENTION.description}
23 |
24 |
25 |
26 | {authors.map((author) => )}
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/pages/categories/[category]/[page].astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { GetStaticPaths } from "astro";
3 | import { SITE } from "@/lib/config";
4 | import { articlesHandler } from "@/lib/handlers/articles";
5 | import { categoriesHandler } from "@/lib/handlers/categories";
6 | import ListLayout from "@/layouts/list.astro";
7 |
8 | import Pagination from "@/components/shared/pagination.astro";
9 | import WideCard from "@/components/cards/wideCard.astro";
10 | import { getEntry } from "astro:content";
11 |
12 | export const getStaticPaths = (async ({ paginate }) => {
13 | const allCategories = categoriesHandler.allCategories();
14 | const allArticles = articlesHandler.allArticles();
15 |
16 | return allCategories.flatMap((category) => {
17 | const filteredArticles = allArticles.filter(
18 | (article) => article.data.category.id === category.id
19 | );
20 | return paginate(filteredArticles, {
21 | params: { category: category.id },
22 | props: { postsLength: filteredArticles.length },
23 | pageSize: SITE.postsPerPage,
24 | });
25 | });
26 | }) satisfies GetStaticPaths;
27 |
28 | const { page } = Astro.props;
29 | const { postsLength } = Astro.props;
30 | const params = Astro.params;
31 | const articles = page.data;
32 | const pathname = new URL(Astro.request.url).pathname.split("/");
33 | const basePath = `${pathname[1]}/${pathname[2]}`;
34 |
35 | const entry = await getEntry("views", "categories");
36 |
37 | if (!entry) {
38 | return Astro.redirect("/404");
39 | }
40 | ---
41 |
42 |
43 |
44 | {
45 | articles.map((article) => (
46 |
50 | ))
51 | }
52 |
53 | {
54 | postsLength > SITE.postsPerPage ? (
55 |
64 | ) : null
65 | }
66 |
67 |
--------------------------------------------------------------------------------
/src/pages/categories/[category]/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { categoriesHandler } from "@/lib/handlers/categories";
3 |
4 | export function getStaticPaths() {
5 | const categories = categoriesHandler.allCategories();
6 | return categories.map((category) => ({ params: { category: category.id } }));
7 | }
8 |
9 | const { category } = Astro.params;
10 | ---
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/pages/categories/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import ListLayout from "@/layouts/list.astro";
3 | import { getEntry } from "astro:content";
4 | import { categoriesHandler } from "@/lib/handlers/categories";
5 | import NewsCard from "@/components/cards/newsCard.astro";
6 | import ArrowRight02 from "@/assets/svgs/arrow-right-02.astro";
7 |
8 | const entry = await getEntry("views", "categories");
9 |
10 | if (!entry) {
11 | return Astro.redirect("/404");
12 | }
13 |
14 | // Fetch all articles to extract categories
15 | const articles = categoriesHandler.allCategoriesWithLatestArticles();
16 | ---
17 |
18 | {/* Use your BaseLayout and pass the entry for metadata */}
19 |
20 |
21 | {
22 | articles.map((category) => {
23 | const { path, title, latestArticles } = category.data;
24 | const articleCount = category.data.count;
25 | return (
26 |
27 |
44 |
45 | {/* {latestArticles.slice(0, 6).map((article, index) => (
46 |
47 | ))} */}
48 |
49 | {latestArticles.slice(0, 6).map((article, index) => (
50 |
51 |
52 |
53 | ))}
54 |
55 |
56 |
57 | );
58 | })
59 | }
60 |
61 |
62 |
--------------------------------------------------------------------------------
/src/pages/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import BaseLayout from "@/layouts/base.astro";
3 | import { getEntry } from "astro:content";
4 | import Headlines from "@/pages/_home/headlines.astro";
5 | import LatestNews from "@/pages/_home/latestNews.astro";
6 | import Authors from "@/pages/_home/authors.astro";
7 |
8 | const entry = await getEntry("views", "home");
9 |
10 | if (!entry) {
11 | return Astro.redirect("/404");
12 | }
13 | ---
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/pages/rss.xml.js:
--------------------------------------------------------------------------------
1 | import rss from "@astrojs/rss";
2 | import { getCollection } from "astro:content";
3 | import { SITE } from "../lib/config";
4 |
5 | export async function GET(context) {
6 | const articles = await getCollection("articles");
7 | return rss({
8 | title: SITE.title,
9 | description: SITE.description,
10 | site: context.site,
11 | items: articles.map((article) => ({
12 | title: article.data.title,
13 | pubDate: article.data.publishedTime,
14 | description: article.data.description,
15 | link: `/articles/${article.id}/`,
16 | })),
17 | });
18 | }
19 |
--------------------------------------------------------------------------------
/src/pages/search/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import BaseLayout from "@/layouts/base.astro";
3 | import "@pagefind/default-ui/css/ui.css";
4 | import { getEntry } from "astro:content";
5 |
6 | const entry = await getEntry("views", "search");
7 |
8 | if (!entry) {
9 | return Astro.redirect("/404");
10 | }
11 | ---
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
76 |
77 |
78 |
79 |
94 |
--------------------------------------------------------------------------------
/src/styles/global.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 | @plugin "@tailwindcss/typography";
3 | @plugin "daisyui" {
4 | themes: light --default, dark --prefersdark;
5 | root: ":root";
6 | logs: false;
7 | }
8 |
9 | @custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
10 |
11 | @theme {
12 | /* Font Family */
13 | --font-sans: "Source Sans Pro", ui-sans-serif, system-ui, sans-serif,
14 | "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
15 | --font-serif: "Source Serif 4 Variable", ui-serif, Georgia, Cambria,
16 | "Times New Roman", Times, serif;
17 |
18 | --breakpoint-2xl: initial;
19 | }
20 |
21 | @layer base {
22 | :root {
23 | @apply cursor-default;
24 | }
25 | html {
26 | @apply h-full text-[16px];
27 | }
28 |
29 | @media (min-width: 1024px) {
30 | html {
31 | @apply text-[17px];
32 | }
33 | }
34 |
35 | body {
36 | @apply font-sans antialiased h-full;
37 | }
38 |
39 | h1,
40 | h2 {
41 | @apply font-serif;
42 | }
43 |
44 | small,
45 | time {
46 | @apply text-sm;
47 | }
48 | .prose {
49 | pre {
50 | code {
51 | span,
52 | p {
53 | @apply break-all;
54 | }
55 | }
56 | }
57 | }
58 | }
59 |
60 | @utility container {
61 | margin-inline: auto;
62 | padding-inline: 1rem;
63 | @media (max-width: 640px) {
64 | max-width: none;
65 | }
66 | @media (min-width: 1280px) {
67 | max-width: 1248px;
68 | }
69 | }
70 |
71 | @layer components {
72 | .a-01 {
73 | @apply hover:underline decoration-primary;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/strict",
3 | "include": [
4 | ".astro/types.d.ts",
5 | "**/*"
6 | ],
7 | "exclude": [
8 | "dist"
9 | ],
10 | "compilerOptions": {
11 | "baseUrl": ".",
12 | "paths": {
13 | "@/*": [
14 | "src/*"
15 | ],
16 | "@assets/*": [
17 | "src/assets/*"
18 | ]
19 | },
20 | "jsx": "react-jsx",
21 | "jsxImportSource": "react"
22 | }
23 | }
--------------------------------------------------------------------------------