├── .npmrc ├── src ├── types │ └── astro.d.ts ├── utils │ └── debug.ts ├── env.d.ts ├── consts.ts ├── layouts │ ├── Base.astro │ ├── TransitionLayout.astro │ ├── BaseLayout.astro │ ├── Layout.astro │ ├── BlogPost.astro │ └── styles │ │ └── markdown.css ├── components │ ├── FormattedDate.astro │ ├── TagList.astro │ ├── Background.astro │ ├── ShareButtons.astro │ ├── Navigation.astro │ └── ThemeToggle.astro ├── content │ ├── config.ts │ └── blog │ │ └── optimizing-web-performance.md ├── pages │ ├── rss.xml.ts │ ├── blog │ │ ├── [...slug].astro │ │ └── index.astro │ ├── 404.astro │ ├── tags │ │ └── [tag].astro │ └── about.astro └── styles │ └── global.css ├── public ├── i.jpg ├── image.png ├── robots.txt ├── placeholder.svg ├── placeholder-logo.png ├── favicon.svg ├── placeholder.jpg ├── placeholder-user.jpg └── independence-palace.svg ├── .env.example ├── postcss.config.mjs ├── .prettierrc ├── .gitignore ├── tsconfig.json ├── package.json ├── LICENSE.md ├── astro.config.mjs ├── tailwind.config.cjs └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | save-exact=true 3 | 4 | -------------------------------------------------------------------------------- /src/types/astro.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/i.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/williamcachamwri/astro-blog/HEAD/public/i.jpg -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | SPOTIFY_CLIENT_ID= 2 | SPOTIFY_CLIENT_SECRET= 3 | SPOTIFY_REFRESH_TOKEN= 4 | -------------------------------------------------------------------------------- /public/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/williamcachamwri/astro-blog/HEAD/public/image.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | 4 | Sitemap: https://your-domain.com/sitemap-index.xml 5 | 6 | -------------------------------------------------------------------------------- /src/utils/debug.ts: -------------------------------------------------------------------------------- 1 | export function debugObject(obj: any): string { 2 | return JSON.stringify(obj, null, 2) 3 | } 4 | 5 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | -------------------------------------------------------------------------------- /src/consts.ts: -------------------------------------------------------------------------------- 1 | // Place any global data in this file. 2 | // You can import this data from anywhere in your site by using the `import` keyword. 3 | 4 | export const SITE_TITLE = 'My Astro Blog'; 5 | export const SITE_DESCRIPTION = 'Welcome to my blog built with Astro'; -------------------------------------------------------------------------------- /public/placeholder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Placeholder Image 4 | 5 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | 'postcss-preset-env': { 7 | features: { 8 | 'nesting-rules': false, 9 | }, 10 | }, 11 | }, 12 | }; 13 | 14 | export default config; 15 | -------------------------------------------------------------------------------- /src/layouts/Base.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from './Layout.astro'; 3 | 4 | export interface Props { 5 | title: string; 6 | description?: string; 7 | } 8 | 9 | const { title, description = "A minimalist personal blog" } = Astro.props; 10 | --- 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "semi": true, 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "trailingComma": "es5", 7 | "useTabs": false, 8 | "plugins": ["prettier-plugin-astro", "prettier-plugin-tailwindcss"], 9 | "overrides": [ 10 | { 11 | "files": "*.astro", 12 | "options": { 13 | "parser": "astro" 14 | } 15 | } 16 | ] 17 | } 18 | 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | .output/ 4 | 5 | # dependencies 6 | node_modules/ 7 | 8 | # logs 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | pnpm-debug.log* 13 | 14 | # environment variables 15 | # Thêm vào file .gitignore hiện có 16 | .env 17 | .env.local 18 | .env.development 19 | .env.production 20 | 21 | # macOS-specific files 22 | .DS_Store 23 | 24 | # Local Netlify folder 25 | .netlify 26 | 27 | # Astro 28 | .astro/ 29 | 30 | .vercel 31 | -------------------------------------------------------------------------------- /src/components/FormattedDate.astro: -------------------------------------------------------------------------------- 1 | --- 2 | export interface Props { 3 | date?: Date | string; 4 | } 5 | 6 | const { date } = Astro.props; 7 | 8 | // Convert string to Date if needed 9 | const parsedDate = typeof date === 'string' ? new Date(date) : date; 10 | --- 11 | 12 | {parsedDate && ( 13 | 20 | )} 21 | 22 | -------------------------------------------------------------------------------- /public/placeholder-logo.png: -------------------------------------------------------------------------------- 1 | �PNG 2 |  3 | IHDR�M��0PLTEZ? tRNS� �@��`P0p���w �IDATx��ؽJ3Q�7'��%�|?� ���E�l�7���(X�D������w`����[�*t����D���mD�}��4; ;�DDDDDDDDDDDD_�_İ��!�y�`�_�:�� ;Ļ�'|� ��;.I"����3*5����J�1�� �T��FI�� ��=��3܃�2~�b���0��U9\��]�4�#w0��Gt\&1 �?21,���o!e�m��ĻR�����5�� ؽAJ�9��R)�5�0.FFASaǃ�T�#|�K���I�������1� 4 | M������N"��$����G�V�T� ��T^^��A�$S��h(�������G]co"J׸^^�'�=���%� �W�6Ы�W��w�a�߇*�^^�YG�c���`'F����������������^5_�,�S�%IEND�B`� -------------------------------------------------------------------------------- /src/content/config.ts: -------------------------------------------------------------------------------- 1 | import { defineCollection, z } from 'astro:content'; 2 | 3 | const blogCollection = defineCollection({ 4 | type: 'content', 5 | schema: z.object({ 6 | title: z.string(), 7 | description: z.string(), 8 | pubDate: z.date(), 9 | updatedDate: z.date().optional(), 10 | heroImage: z.string().optional(), 11 | tags: z.array(z.string()).optional(), 12 | // Thêm readingTime vào schema nhưng đặt là optional 13 | readingTime: z.string().optional(), 14 | }), 15 | }); 16 | 17 | export const collections = { 18 | 'blog': blogCollection, 19 | }; 20 | 21 | -------------------------------------------------------------------------------- /src/components/TagList.astro: -------------------------------------------------------------------------------- 1 | --- 2 | export interface Props { 3 | tags: string[]; 4 | class?: string; // Add optional class prop 5 | } 6 | 7 | const { tags = [], class: className = '' } = Astro.props; 8 | --- 9 | 10 | {tags.length > 0 && ( 11 |
12 | {tags.map(tag => ( 13 | 17 | {tag} 18 | 19 | ))} 20 |
21 | )} -------------------------------------------------------------------------------- /src/layouts/TransitionLayout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { ViewTransitions } from 'astro:transitions'; 3 | import BaseLayout from './BaseLayout.astro'; 4 | 5 | const { title, description } = Astro.props; 6 | --- 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 |
15 | 16 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "compilerOptions": { 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "target": "ES6", 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "bundler", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "baseUrl": ".", 18 | "plugins": [ 19 | { 20 | "name": "next" 21 | } 22 | ], 23 | "paths": { 24 | "@/*": ["src/*"] 25 | } 26 | }, 27 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 28 | "exclude": ["node_modules"] 29 | } 30 | 31 | -------------------------------------------------------------------------------- /src/pages/rss.xml.ts: -------------------------------------------------------------------------------- 1 | import rss from '@astrojs/rss'; 2 | import { getCollection } from 'astro:content'; 3 | import { SITE_TITLE, SITE_DESCRIPTION } from '../consts'; 4 | 5 | // Change back to uppercase GET to match what Astro is expecting in newer versions 6 | export async function GET(context) { 7 | const posts = await getCollection('blog'); 8 | 9 | // Sort posts by date in descending order 10 | const sortedPosts = posts.sort( 11 | (a, b) => new Date(b.data.pubDate).valueOf() - new Date(a.data.pubDate).valueOf() 12 | ); 13 | 14 | return rss({ 15 | title: SITE_TITLE, 16 | description: SITE_DESCRIPTION, 17 | site: context.site, 18 | items: sortedPosts.map((post) => ({ 19 | title: post.data.title, 20 | pubDate: post.data.pubDate, 21 | description: post.data.description, 22 | link: `/blog/${post.slug}/`, 23 | // Optional: include categories/tags as array 24 | categories: post.data.tags || [], 25 | })), 26 | }); 27 | } -------------------------------------------------------------------------------- /public/placeholder.jpg: -------------------------------------------------------------------------------- 1 | ����JFIFHH���ExifMM*JR(�iZHH�����8Photoshop 3.08BIM8BIM%��ُ�� ���B~���� 2 | ���s!1"AQ2aq#� �B�R3�$b0�r�C�4��S@%c5�s�PD���&T6d�t�`҄�p�'E7e�Uu��Å��Fv��GVf� 3 | ()*89:HIJWXYZghijwxyz����������������������������������������������������������� 4 | ����! 1A0"2Q@3#aBqR4�P$��C�b5S��%`�D�r��c6p&ET�'�� 5 | ()*789:FGHIJUVWXYZdefghijstuvwxyz�����������������������������������������������������������������������������C  6 |  7 | 8 | ")$+*($''-2@7-0=0''8L9=CEHIH+6OUNFT@GHE��C !!E.'.EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE�� �k����?��?��?��3 !1AQaq��������� 0@P`p���������?!��� ��3 !1AQa q𑁡�����0@P`p���������?���?���?��� -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "minimalist-astro-blog", 3 | "type": "module", 4 | "version": "0.1.0", 5 | "private": true, 6 | "scripts": { 7 | "dev": "astro dev", 8 | "start": "astro dev", 9 | "build": "astro build", 10 | "preview": "astro preview", 11 | "astro": "astro" 12 | }, 13 | "dependencies": { 14 | "@astrojs/db": "0.14.10", 15 | "@astrojs/mdx": "4.2.3", 16 | "@astrojs/node": "9.1.3", 17 | "@astrojs/react": "4.2.3", 18 | "@astrojs/rss": "3.0.0", 19 | "@astrojs/sitemap": "^3.0.0", 20 | "@astrojs/tailwind": "6.0.2", 21 | "astro": "5.5.6", 22 | "framer-motion": "12.6.3", 23 | "react": "19.1.0", 24 | "react-dom": "19.1.0", 25 | "react-hotkeys-hook": "4.6.1", 26 | "react-icons": "5.5.0", 27 | "reading-time": "1.5.0", 28 | "sanitize-html": "2.15.0", 29 | "tailwindcss": "^3.3.3" 30 | }, 31 | "devDependencies": { 32 | "@tailwindcss/typography": "^0.5.9", 33 | "prettier": "^3.0.3", 34 | "prettier-plugin-astro": "^0.12.0", 35 | "prettier-plugin-tailwindcss": "^0.5.4" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2025 Lê Vĩnh Khang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'astro/config'; 2 | import tailwind from '@astrojs/tailwind'; 3 | import react from '@astrojs/react'; // Add this line 4 | 5 | // Determine site URL based on environment 6 | const getSiteURL = () => { 7 | // For Vercel production deployment 8 | if (process.env.VERCEL_URL) { 9 | return `https://${process.env.VERCEL_URL}`; 10 | } 11 | // For Vercel preview deployment 12 | if (process.env.VERCEL_BRANCH_URL) { 13 | return `https://${process.env.VERCEL_BRANCH_URL}`; 14 | } 15 | // For local development 16 | return 'http://localhost:4321'; 17 | }; 18 | 19 | // https://astro.build/config 20 | export default defineConfig({ 21 | site: getSiteURL(), 22 | integrations: [ 23 | tailwind(), 24 | react(), 25 | ], 26 | vite: { 27 | // Đảm bảo biến môi trường được chuyển đến client 28 | define: { 29 | 'import.meta.env.SPOTIFY_CLIENT_ID': JSON.stringify(process.env.SPOTIFY_CLIENT_ID), 30 | 'import.meta.env.SPOTIFY_CLIENT_SECRET': JSON.stringify(process.env.SPOTIFY_CLIENT_SECRET), 31 | 'import.meta.env.SPOTIFY_REFRESH_TOKEN': JSON.stringify(process.env.SPOTIFY_REFRESH_TOKEN), 32 | }, 33 | }, 34 | }); -------------------------------------------------------------------------------- /public/placeholder-user.jpg: -------------------------------------------------------------------------------- 1 | ����JFIF��C 2 |   3 |  4 | $ &%# #"(-90(*6+"#2D26;=@@@&0FKE>J9?@=��C  =)#)==================================================���������� ـ|�r4�-�̈"x�'�0�Í��8�H�N�q�����Q�������V�`=�($q"_�� 5 | �S8�P��0VFbP��! 6 | Io40��[?p#�|� @!.E�3��4pBq �Z s���C  AQR�!1@Ua��02Tq���56cps�� "#$PS�����?��R���,�� ��� 7 | �n�k��n8rZ�����9Vv��V��ms$9zWʏh�-+@Z�2�PGE������EY9��i�Ͻ�S ��O��Ȕ��_I��W髵�}�����B�ՎT��>%r �[e/,W�D}��D�>b�e>�v�Z�p&�*VS��V�sV�c�:��~K�������C��:��k�'An|ʶ�}\��C� �f����a�;�h��J����q!i=���"�q�NF�IZ�`�wĝ5hAj� 8 | �RXl䎉�lk���@I�%l��Ն���FDY-����Eq�i����O�I�_�2b�lNj�Yu��k���AO����٣��ܭ�n��cam�jN�j���VL�}� ;��oކ6��շs��,���ք���l�i����l{I�O��(!%J $ ���n�-@G����n��ܮi!�괁G�:�^��n�g3l�F%���9]�Pq��)�:��� @�*ɍmׅ�VLY'�s+�z ���V�m�J9��S�_���#��;�����NJ!5�#�q\�M@��@�]yz�����A;e�k��@�s�^���G����\�5F��(��S��Ly���c�i8�����o�8T��i�N��7D����t-�p�3`r�q r�;|�.��bTG��[i H��͚-� 9 | ��Oj�H����M�ؒFE�{�3X�n���e� �R3/�~����� 10 | ��a����!�j&@^r�����Y�������l�Z? �7땵��)ki�w��\.�u�����X��\.�u�����X��\.�u�����X��\.�u��p�M����o(N��3�Vg�����Z�s��%�\�]q}d�k\_Y5���MG�����Q��q}d�k\_Y5���MG�����Q��q}d�kV|5���8���//�������?�����?�� -------------------------------------------------------------------------------- /tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}", "*.{js,ts,jsx,tsx,mdx}"], 4 | darkMode: 'class', 5 | theme: { 6 | extend: { 7 | typography: (theme) => ({ 8 | DEFAULT: { 9 | css: { 10 | a: { 11 | color: theme('colors.zinc.900'), 12 | '&:hover': { 13 | color: theme('colors.zinc.700'), 14 | }, 15 | textDecoration: 'underline', 16 | textDecorationColor: theme('colors.zinc.400'), 17 | textUnderlineOffset: '2px', 18 | }, 19 | 'h1, h2, h3, h4, h5, h6': { 20 | color: theme('colors.zinc.900'), 21 | }, 22 | code: { 23 | color: theme('colors.zinc.900'), 24 | backgroundColor: theme('colors.zinc.100'), 25 | borderRadius: theme('borderRadius.md'), 26 | padding: `${theme('padding.1')} ${theme('padding.1.5')}`, 27 | }, 28 | 'code::before': { 29 | content: '""', 30 | }, 31 | 'code::after': { 32 | content: '""', 33 | }, 34 | }, 35 | }, 36 | invert: { 37 | css: { 38 | a: { 39 | color: theme('colors.zinc.100'), 40 | '&:hover': { 41 | color: theme('colors.zinc.300'), 42 | }, 43 | textDecorationColor: theme('colors.zinc.700'), 44 | }, 45 | 'h1, h2, h3, h4, h5, h6': { 46 | color: theme('colors.zinc.100'), 47 | }, 48 | code: { 49 | color: theme('colors.zinc.100'), 50 | backgroundColor: theme('colors.zinc.800'), 51 | }, 52 | }, 53 | }, 54 | }), 55 | }, 56 | }, 57 | plugins: [ 58 | require('@tailwindcss/typography'), 59 | ], 60 | }; 61 | 62 | -------------------------------------------------------------------------------- /src/layouts/BaseLayout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from './Layout.astro'; 3 | 4 | export interface Props { 5 | title: string; 6 | description?: string; 7 | } 8 | 9 | const { title, description = "A minimalist personal blog" } = Astro.props; 10 | --- 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/content/blog/optimizing-web-performance.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Optimizing Web Performance for Better UX" 3 | description: "Tips and techniques for improving your website's performance and providing a better user experience." 4 | pubDate: 2023-01-18 5 | heroImage: "/i.jpg" 6 | readingTime: "8 min read" 7 | tags: ["performance", "web development", "user experience"] 8 | --- 9 | 10 | # Optimizing Web Performance for Better UX 11 | 12 | Web performance is a critical aspect of user experience. Studies consistently show that users abandon sites that take too long to load, and search engines like Google factor page speed into their ranking algorithms. In this post, we'll explore practical strategies to optimize your website's performance. 13 | 14 | ## Why Performance Matters 15 | 16 | Before diving into optimization techniques, let's understand why performance is crucial: 17 | 18 | - **User Experience**: 53% of mobile users abandon sites that take longer than 3 seconds to load 19 | - **Conversion Rates**: A 1-second delay in page load time can result in a 7% reduction in conversions 20 | - **SEO**: Page speed is a ranking factor for search engines 21 | - **Accessibility**: Fast websites are more accessible, especially for users with limited bandwidth 22 | 23 | ## Core Web Vitals 24 | 25 | Google's Core Web Vitals are a set of specific factors that Google considers important for a webpage's overall user experience: 26 | 27 | 1. **Largest Contentful Paint (LCP)**: Measures loading performance. To provide a good user experience, LCP should occur within 2.5 seconds of when the page first starts loading. 28 | 29 | 2. **First Input Delay (FID)**: Measures interactivity. Pages should have a FID of less than 100 milliseconds. 30 | 31 | 3. **Cumulative Layout Shift (CLS)**: Measures visual stability. Pages should maintain a CLS of less than 0.1. 32 | 33 | ## Performance Optimization Techniques 34 | 35 | ### 1. Optimize Images 36 | 37 | Images often account for most of the downloaded bytes on a webpage. Optimizing them can significantly improve load times: 38 | 39 | - Use modern formats like WebP or AVIF 40 | - Implement responsive images with `srcset` and `sizes` attributes 41 | - Lazy load images below the fold 42 | - Compress images without sacrificing quality 43 | 44 | ```html 45 | Description 52 | ``` 53 | -------------------------------------------------------------------------------- /src/styles/global.css: -------------------------------------------------------------------------------- 1 | /* Remove all the complex mobile menu styles and keep only what's necessary */ 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | 6 | @layer base { 7 | :root { 8 | font-family: "Inter", sans-serif; 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | --theme-transition: 0.3s ease; 12 | } 13 | 14 | html { 15 | scroll-behavior: smooth; 16 | scroll-padding-top: 5rem; 17 | } 18 | 19 | body { 20 | @apply min-h-screen bg-white text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100; 21 | margin: 0; 22 | padding: 0; 23 | overflow-x: hidden; 24 | } 25 | 26 | /* Simple theme transition */ 27 | body, a, button { 28 | transition: background-color var(--theme-transition), 29 | color var(--theme-transition), 30 | border-color var(--theme-transition); 31 | } 32 | } 33 | 34 | /* Minimal responsive styles */ 35 | @media (max-width: 640px) { 36 | html { 37 | scroll-padding-top: 4rem; 38 | } 39 | 40 | /* Better touch targets on mobile */ 41 | button, a { 42 | @apply min-h-[44px]; 43 | } 44 | } 45 | 46 | 47 | /* Add smooth animations */ 48 | @keyframes fadeIn { 49 | from { opacity: 0; } 50 | to { opacity: 1; } 51 | } 52 | 53 | @keyframes slideUp { 54 | from { transform: translateY(20px); opacity: 0; } 55 | to { transform: translateY(0); opacity: 1; } 56 | } 57 | 58 | @keyframes slideDown { 59 | from { transform: translateY(-20px); opacity: 0; } 60 | to { transform: translateY(0); opacity: 1; } 61 | } 62 | 63 | @keyframes scaleIn { 64 | from { transform: scale(0.95); opacity: 0; } 65 | to { transform: scale(1); opacity: 1; } 66 | } 67 | 68 | /* Apply animations to elements */ 69 | .animate-fade-in { 70 | animation: fadeIn 0.6s ease forwards; 71 | } 72 | 73 | .animate-slide-up { 74 | animation: slideUp 0.6s ease forwards; 75 | } 76 | 77 | .animate-slide-down { 78 | animation: slideDown 0.6s ease forwards; 79 | } 80 | 81 | .animate-scale-in { 82 | animation: scaleIn 0.6s ease forwards; 83 | } 84 | 85 | /* Staggered animation delays */ 86 | .delay-100 { 87 | animation-delay: 0.1s; 88 | } 89 | 90 | .delay-200 { 91 | animation-delay: 0.2s; 92 | } 93 | 94 | .delay-300 { 95 | animation-delay: 0.3s; 96 | } 97 | 98 | .delay-400 { 99 | animation-delay: 0.4s; 100 | } 101 | 102 | /* Smooth hover transitions */ 103 | a, button { 104 | transition: all 0.2s ease; 105 | } 106 | 107 | a:hover, button:hover { 108 | transform: translateY(-1px); 109 | } 110 | 111 | /* Smooth page transitions */ 112 | .page-transition { 113 | transition: opacity 0.3s ease, transform 0.3s ease; 114 | } 115 | 116 | .page-entering { 117 | opacity: 0; 118 | transform: translateY(10px); 119 | } 120 | 121 | .page-entered { 122 | opacity: 1; 123 | transform: translateY(0); 124 | } 125 | -------------------------------------------------------------------------------- /src/components/Background.astro: -------------------------------------------------------------------------------- 1 | --- 2 | // Background.astro - Dot pattern and ambient glow background with smooth theme transitions 3 | --- 4 | 5 |
6 | 7 |
8 | 9 | 10 |
11 |
12 | 13 | 14 |
15 |
16 | 17 | 45 | 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # ✨ Astro Blog 3 | 4 |

5 | Astro Blog Logo 6 |

7 | 8 |

9 | A modern, minimalist, high-performance blog platform built with Astro.js 10 |

11 | 12 |

13 | Demo • 14 | Features • 15 | Getting Started • 16 | Project Structure • 17 | Customization • 18 | Deployment • 19 |

20 | 21 |

22 | Astro Blog Screenshot 23 |

24 | 25 | ## Demo 26 | 27 | [View Live Demo](https://astro-blog-pi-ashen.vercel.app/) 28 | 29 | ## Features 30 | 31 | - 🚀 **Maximum Performance** - Built with Astro.js for lightning-fast static sites 32 | - 🎨 **Minimalist Design** - Clean UI that focuses on content 33 | - 🌓 **Light/Dark Mode** - Smooth theme switching 34 | - 📱 **Responsive** - Perfect experience on all devices 35 | - ⚡ **SPA Transitions** - Smooth page navigation with transition effects 36 | - 📝 **Markdown & MDX** - Write posts with Markdown and extend with MDX 37 | - 🔍 **SEO Optimized** - Meta tags, Open Graph, and Twitter Cards 38 | - 📊 **Analytics** - Reading time, views, and statistics 39 | - 🔖 **Categorization** - Tags and categories system 40 | - 🔄 **RSS Feed** - Automatically generated RSS feed 41 | - 🎵 **Spotify Integration** - Display currently playing track 42 | - 🌐 **Internationalization Ready** - Prepared for multiple languages 43 | - 🔒 **Secure** - No unnecessary client-side JavaScript 44 | 45 | ## Getting Started 46 | 47 | ### Requirements 48 | 49 | - Node.js 16+ and npm/yarn 50 | - Spotify account (optional, for Now Playing feature) 51 | 52 | ### Installation 53 | 54 | ```bash 55 | # Clone repository 56 | git clone https://github.com/williamcachamwri/astro-blog 57 | 58 | # Navigate to project directory 59 | cd astro-blog 60 | 61 | # Install dependencies 62 | npm install 63 | 64 | # Create .env file from template 65 | cp .env.example .env 66 | 67 | # Edit .env with your information 68 | ``` 69 | 70 | ### Development 71 | 72 | ```bash 73 | # Start development server 74 | npm run dev 75 | 76 | # Open browser at http://localhost:4321 77 | ``` 78 | 79 | ### Build 80 | 81 | ```bash 82 | # Create production build 83 | npm run build 84 | 85 | # Preview production build 86 | npm run preview 87 | ``` 88 | 89 | ## Project Structure 90 | 91 | ``` 92 | / 93 | ├── public/ # Static assets 94 | ├── src/ 95 | │ ├── components/ # Reusable UI components 96 | │ ├── content/ # Blog content (Markdown/MDX) 97 | │ ├── layouts/ # Page layouts 98 | │ ├── pages/ # Pages and routes 99 | │ ├── styles/ # CSS and Tailwind 100 | │ └── utils/ # Utilities and helpers 101 | ├── astro.config.mjs # Astro configuration 102 | ├── tailwind.config.js # Tailwind configuration 103 | └── tsconfig.json # TypeScript configuration 104 | ``` 105 | 106 | ## Customization 107 | 108 | ### Changing Theme 109 | 110 | Edit `tailwind.config.js` to change colors, fonts, and other design variables: 111 | 112 | ```js 113 | // tailwind.config.js 114 | module.exports = { 115 | theme: { 116 | extend: { 117 | colors: { 118 | primary: {...}, 119 | secondary: {...} 120 | }, 121 | fontFamily: { 122 | sans: ['Inter', ...], 123 | serif: [...] 124 | } 125 | } 126 | } 127 | } 128 | ``` 129 | 130 | ### Adding New Posts 131 | 132 | Create a new Markdown or MDX file in the `src/content/blog` directory: 133 | 134 | ```md 135 | --- 136 | title: "Optimizing Web Performance for Better UX" 137 | description: "Tips and techniques for improving your website's performance and providing a better user experience." 138 | pubDate: 2023-01-18 139 | heroImage: "/placeholder.svg?height=630&width=1200" 140 | readingTime: "8 min read" 141 | tags: ["performance", "web development", "user experience"] 142 | --- 143 | 144 | Your post content here... 145 | ``` 146 | 147 | ## Spotify Integration 148 | 149 | To enable the "Now Playing" feature from Spotify: 150 | 151 | 1. Create an app at [Spotify Developer Dashboard](https://developer.spotify.com/dashboard/) 152 | 2. Get your Client ID and Client Secret 153 | 3. Add them to your `.env` file: 154 | 155 | ```env 156 | SPOTIFY_CLIENT_ID=your_client_id 157 | SPOTIFY_CLIENT_SECRET=your_client_secret 158 | SPOTIFY_REFRESH_TOKEN=your_refresh_token 159 | ``` 160 | 161 | ## Deployment 162 | 163 | ### Netlify 164 | 165 | [![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/williamcachamwri/astro-blog) 166 | 167 | ### Vercel 168 | 169 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/williamcachamwri/astro-blog) 170 | --- 171 | 172 |

173 | Made with ❤️ by William Cachamwri 174 |

175 | -------------------------------------------------------------------------------- /src/components/ShareButtons.astro: -------------------------------------------------------------------------------- 1 | --- 2 | export interface Props { 3 | title: string; 4 | url: string; 5 | class?: string; // Add optional class prop 6 | } 7 | 8 | const { title, url, class: className = '' } = Astro.props; 9 | const encodedTitle = encodeURIComponent(title); 10 | const encodedUrl = encodeURIComponent(url); 11 | --- 12 | 13 |
14 | Share: 15 |
16 | 23 | 24 | 25 | 32 | 33 | 34 | 41 | 42 | 43 | 54 |
55 |
56 | 57 | -------------------------------------------------------------------------------- /src/components/Navigation.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import ThemeToggle from './ThemeToggle.astro'; 3 | 4 | const navItems = [ 5 | { text: 'Home', href: '/' }, 6 | { text: 'Blog', href: '/blog' }, 7 | { text: 'Tags', href: '/tags' }, 8 | { text: 'About', href: '/about' }, 9 | { text: 'RSS', href: 'rss.xml' }, 10 | ]; 11 | 12 | // Get current path for active link highlighting 13 | const pathname = new URL(Astro.request.url).pathname; 14 | const currentPath = pathname.slice(1); // remove the first "/" 15 | --- 16 | 17 |
18 |
19 | 20 | JD 21 | 22 | 23 | 39 | 40 | 41 | 46 |
47 |
48 | 49 | 50 |
51 |
52 | JD 53 | 58 |
59 | 60 | 79 |
80 | 81 | 82 |
83 | 84 | 175 | 176 | 202 | 203 | -------------------------------------------------------------------------------- /src/components/ThemeToggle.astro: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | 44 | 45 | 192 | 193 | 294 | 295 | -------------------------------------------------------------------------------- /src/layouts/Layout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Navigation from '../components/Navigation.astro'; 3 | import Footer from '../components/Footer.astro'; 4 | import Background from '../components/Background.astro'; 5 | import '../styles/global.css'; 6 | 7 | export interface Props { 8 | title: string; 9 | description?: string; 10 | } 11 | 12 | const { title, description = "A minimalist personal blog" } = Astro.props; 13 | --- 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {title} 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
32 |
33 | 34 | 35 | 36 | 37 |
38 | 39 |
40 | 41 |
42 |
43 |