├── .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 |
14 | {parsedDate.toLocaleDateString('en-us', {
15 | year: 'numeric',
16 | month: 'long',
17 | day: 'numeric',
18 | })}
19 |
20 | )}
21 |
22 |
--------------------------------------------------------------------------------
/public/placeholder-logo.png:
--------------------------------------------------------------------------------
1 | �PNG
2 |
3 |
IHDR � M�� 0PLTE Z? 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 |
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 | ���� JFIF H H �� �Exif MM * J R( �i Z H H � � � �� 8Photoshop 3.0 8BIM 8BIM% ��ُ �� ���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��0 VFbP��!
6 | Io40 ��[?p #�|�@ !.E� 3��4p Bq �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 |
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 |
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 |
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 | [](https://app.netlify.com/start/deploy?repository=https://github.com/williamcachamwri/astro-blog)
166 |
167 | ### Vercel
168 |
169 | [](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 |
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 |
24 | {navItems.map(item => {
25 | const isActive = currentPath === (item.href === '/' ? '' : item.href.slice(1));
26 | return (
27 |
33 | {item.text}
34 |
35 | )
36 | })}
37 |
38 |
39 |
40 |
41 |
46 |
47 |
48 |
49 |
50 |
80 |
81 |
82 |
83 |
84 |
175 |
176 |
202 |
203 |
--------------------------------------------------------------------------------
/src/components/ThemeToggle.astro:
--------------------------------------------------------------------------------
1 | ---
2 | ---
3 |
4 |
10 |
11 |
12 |
22 |
23 |
24 |
25 |
26 |
27 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
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 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
262 |
263 |
264 |
265 |
--------------------------------------------------------------------------------
/src/pages/blog/[...slug].astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getCollection } from 'astro:content';
3 | import BlogPost from '../../layouts/BlogPost.astro';
4 | import readingTime from 'reading-time'; // Thêm import này nếu chưa có
5 |
6 | export async function getStaticPaths() {
7 | const blogEntries = await getCollection('blog');
8 |
9 | // Sort entries by date for better next/prev navigation
10 | const sortedEntries = [...blogEntries].sort(
11 | (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
12 | );
13 |
14 | return sortedEntries.map((entry, index) => {
15 | // Tính toán readingTime từ nội dung markdown
16 | const readingTimeResult = readingTime(entry.body);
17 |
18 | // Định dạng thời gian đọc thành chuỗi dễ đọc
19 | let readingTimeStr;
20 | if (readingTimeResult.minutes < 1) {
21 | readingTimeStr = 'Dưới 1 phút đọc';
22 | } else {
23 | // Làm tròn lên và định dạng thành "X phút đọc"
24 | const minutes = Math.ceil(readingTimeResult.minutes);
25 | readingTimeStr = `${minutes} phút đọc`;
26 | }
27 |
28 | return {
29 | params: { slug: entry.slug },
30 | props: {
31 | entry,
32 | readingTimeValue: readingTimeStr, // Truyền chuỗi đã định dạng
33 | readingTimeStats: readingTimeResult, // Truyền toàn bộ thông tin thống kê
34 | nextPost: index > 0 ? sortedEntries[index - 1] : null,
35 | prevPost: index < sortedEntries.length - 1 ? sortedEntries[index + 1] : null
36 | },
37 | };
38 | });
39 | }
40 |
41 | const { entry, nextPost, prevPost, readingTimeValue, readingTimeStats } = Astro.props;
42 | const { Content } = await entry.render();
43 |
44 | // Ghi đè readingTime từ frontmatter nếu có, nếu không thì sử dụng giá trị đã tính
45 | const finalReadingTime = entry.data.readingTime || readingTimeValue;
46 | ---
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
92 |
93 |
94 |
220 |
221 |
--------------------------------------------------------------------------------
/src/pages/404.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Layout from '../layouts/Layout.astro';
3 | ---
4 |
5 |
6 |
7 |
8 |
13 |
14 |
15 |
16 |
17 |
404
18 |
19 |
20 |
Page Not Found
21 |
22 |
23 | The page you're looking for has vanished into the digital void.
24 |
25 |
26 |
48 |
49 |
50 |
51 |
Did you know?
52 |
53 | The 404 error code originated when CERN's web server displayed room 404 (their server room) as the error message when a file wasn't found.
54 |
55 |
56 |
57 |
58 |
59 |
60 |
143 |
144 |
--------------------------------------------------------------------------------
/src/layouts/BlogPost.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Layout from './Layout.astro';
3 | import FormattedDate from '../components/FormattedDate.astro';
4 | import ShareButtons from '../components/ShareButtons.astro';
5 | import TagList from '../components/TagList.astro';
6 | import './styles/markdown.css';
7 | export interface Props {
8 | title: string;
9 | description: string;
10 | pubDate: Date;
11 | updatedDate?: Date;
12 | heroImage?: string;
13 | readingTime?: string;
14 | tags?: string[];
15 | }
16 |
17 | const { title, description, pubDate, updatedDate, heroImage, readingTime = '5 min read', tags = [] } = Astro.props;
18 |
19 | // Fix for the canonicalURL creation
20 | let canonicalURL;
21 | try {
22 | canonicalURL = new URL(Astro.url.pathname, Astro.site || 'https://example.com');
23 | } catch (error) {
24 | console.error('Error creating canonical URL:', error);
25 | canonicalURL = new URL('https://example.com');
26 | }
27 |
28 | const slug = Astro.url.pathname.split('/').filter(Boolean).pop() || ''; // Ensure slug is a string
29 |
30 | // Xử lý heroImage an toàn hơn - hỗ trợ cả URL tuyệt đối và đường dẫn tương đối
31 | let heroImageUrl = null;
32 | if (heroImage) {
33 | try {
34 | // Kiểm tra nếu là URL đầy đủ
35 | if (heroImage.startsWith('http')) {
36 | heroImageUrl = heroImage;
37 | }
38 | // Kiểm tra nếu là đường dẫn tương đối
39 | else if (heroImage.startsWith('/')) {
40 | // Nếu Astro.site tồn tại, sử dụng nó làm cơ sở
41 | if (Astro.site) {
42 | heroImageUrl = new URL(heroImage, Astro.site).toString();
43 | }
44 | // Nếu không, sử dụng đường dẫn tương đối như đã cung cấp
45 | else {
46 | heroImageUrl = heroImage;
47 | }
48 | }
49 | // Trường hợp khác
50 | else {
51 | heroImageUrl = heroImage;
52 | }
53 | } catch (error) {
54 | console.error(`Error processing heroImage: ${heroImage}`, error);
55 | // Sử dụng đường dẫn gốc nếu có lỗi
56 | heroImageUrl = heroImage;
57 | }
58 | }
59 | ---
60 |
61 |
62 |
63 |
64 |
65 | {title}
66 |
67 |
68 |
69 |
70 | {readingTime && · {readingTime} }
71 |
72 |
73 |
74 |
75 |
76 |
77 | {heroImage && (
78 |
79 |
80 |
86 |
87 |
88 |
89 | )}
90 |
91 |
92 |
93 |
94 |
95 |
96 |
101 |
102 | {updatedDate && (
103 |
104 | Last updated on
105 |
106 | )}
107 |
108 |
109 |
110 |
111 |
112 |
382 |
383 |
413 |
414 |
--------------------------------------------------------------------------------
/src/pages/blog/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getCollection } from 'astro:content';
3 | import BaseLayout from '../../layouts/BaseLayout.astro';
4 |
5 | const allPosts = await getCollection('blog') || [];
6 | const sortedPosts = allPosts.sort(
7 | (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
8 | );
9 |
10 | // Group posts by year for timeline effect
11 | const postsByYear = sortedPosts.reduce((acc, post) => {
12 | const year = new Date(post.data.pubDate).getFullYear();
13 | if (!acc[year]) acc[year] = [];
14 | acc[year].push(post);
15 | return acc;
16 | }, {});
17 |
18 | const years = Object.keys(postsByYear).sort((a, b) => b - a);
19 |
20 | // Get total post count
21 | const totalPosts = sortedPosts.length;
22 |
23 | // Get unique tags for search suggestions
24 | const allTags = [...new Set(sortedPosts.flatMap(post => post.data.tags || []))];
25 | ---
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | Journal
38 |
39 |
40 |
41 | Thoughts, ideas, and explorations on design and technology.
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | {sortedPosts.length > 0 && (
50 |
51 |
52 |
53 | {sortedPosts[0].data.heroImage && (
54 |
55 |
61 |
62 | )}
63 |
64 |
65 |
66 | Featured
67 |
68 | {sortedPosts[0].data.pubDate && (
69 |
70 | {sortedPosts[0].data.pubDate.toLocaleDateString('en-US', {
71 | year: 'numeric',
72 | month: 'long',
73 | day: 'numeric'
74 | })}
75 |
76 | )}
77 |
78 |
79 |
84 |
85 |
86 | {sortedPosts[0].data.description}
87 |
88 |
89 |
90 |
91 |
92 |
93 | {sortedPosts[0].data.readingTime || "5 min read"}
94 |
95 |
96 |
97 | {sortedPosts[0].data.tags && (
98 |
99 | {sortedPosts[0].data.tags.slice(0, 2).map((tag) => (
100 |
101 | {tag}
102 |
103 | ))}
104 |
105 | )}
106 |
107 |
108 |
109 |
110 |
111 | )}
112 |
113 |
114 |
115 |
116 |
Archive
117 |
118 |
119 |
132 |
133 |
134 |
135 |
136 |
137 | {years.map((year) => (
138 |
203 | ))}
204 |
205 |
206 |
207 |
208 |
209 |
287 |
288 |
--------------------------------------------------------------------------------
/src/pages/tags/[tag].astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getCollection } from 'astro:content';
3 | import BaseLayout from '../../layouts/BaseLayout.astro';
4 | import FormattedDate from '../../components/FormattedDate.astro';
5 |
6 | // Add this near the top of the frontmatter section
7 | export const prerender = true;
8 |
9 | export async function getStaticPaths() {
10 | const allPosts = await getCollection('blog');
11 |
12 | // Get all unique tags
13 | const uniqueTags = [...new Set(allPosts.flatMap(post => post.data.tags || []))];
14 |
15 | // Create a path for each tag
16 | return uniqueTags.map(tag => {
17 | // Make tag matching case-insensitive
18 | const filteredPosts = allPosts.filter(post =>
19 | post.data.tags?.some(t => t.toLowerCase() === (tag as string).toLowerCase()) // Explicitly cast tag to string
20 | );
21 | return {
22 | params: { tag },
23 | props: { posts: filteredPosts },
24 | };
25 | });
26 | }
27 |
28 | const { tag } = Astro.params as { tag: string }; // Explicitly type tag as string
29 | const { posts = [] } = Astro.props;
30 |
31 | // Kiểm tra và log số lượng bài viết để debug
32 | console.log(`Tag: ${tag}, Number of posts: ${posts.length}`);
33 |
34 | // Sort posts by date (only if posts array exists and has items)
35 | const sortedPosts = posts && posts.length > 0
36 | ? [...posts].sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf())
37 | : [];
38 |
39 | // Log sau khi sắp xếp để kiểm tra
40 | console.log(`Sorted posts length: ${sortedPosts.length}`);
41 |
42 | // Generate a consistent but random-looking hue for the tag
43 | const tagHue = Math.abs(tag.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % 360);
44 |
45 | // Generate related tags (tags that appear together with the current tag)
46 | const relatedTags = [...new Set(
47 | sortedPosts.flatMap(post => post.data.tags || [])
48 | .filter(t => t !== tag)
49 | )].slice(0, 5);
50 | ---
51 |
52 |
53 |
54 |
55 |
90 |
91 |
92 | {relatedTags.length > 0 && (
93 |
106 | )}
107 |
108 |
109 |
110 |
111 |
112 |
113 | {sortedPosts.map((post) => (
114 |
115 |
116 |
117 |
118 | {post.data.image && (
119 |
120 |
126 |
127 | )}
128 |
129 |
130 |
131 |
132 | {post.data.pubDate && (
133 |
134 |
135 |
137 |
138 |
139 |
140 | )}
141 |
142 | {post.data.readingTime && (
143 |
144 |
145 |
146 |
147 | {post.data.readingTime}
148 |
149 | )}
150 |
151 |
152 |
153 |
154 |
159 |
160 |
161 | {post.data.description}
162 |
163 |
164 |
165 |
166 |
167 | {post.data.tags && post.data.tags.length > 0 && (
168 |
169 | {post.data.tags.slice(0, 3).map(postTag => (
170 |
178 | #{postTag}
179 |
180 | ))}
181 | {post.data.tags.length > 3 && (
182 |
183 | +{post.data.tags.length - 3}
184 |
185 | )}
186 |
187 | )}
188 |
189 |
205 |
206 |
207 | ))}
208 |
209 |
210 |
211 |
212 | {sortedPosts.length === 0 && (
213 |
214 |
219 |
No posts found
220 |
There are no posts with this tag yet.
221 |
222 |
223 |
224 |
225 | Browse all articles
226 |
227 |
228 | )}
229 |
230 |
231 |
232 |
321 |
322 |
403 |
404 |
405 |
--------------------------------------------------------------------------------
/public/independence-palace.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 |
357 |
358 |
--------------------------------------------------------------------------------
/src/pages/about.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import BaseLayout from '../layouts/BaseLayout.astro';
3 | import { FaJs, FaReact, FaNodeJs, FaPython } from 'react-icons/fa';
4 | import { SiTypescript, SiAstro } from 'react-icons/si';
5 |
6 | // Thông tin kỹ năng với logo ngôn ngữ lập trình
7 | const skills = [
8 | {
9 | name: "JavaScript",
10 | level: 90,
11 | icon: FaJs
12 | },
13 | {
14 | name: "TypeScript",
15 | level: 85,
16 | icon: SiTypescript
17 | },
18 | {
19 | name: "React",
20 | level: 88,
21 | icon: FaReact
22 | },
23 | {
24 | name: "Node.js",
25 | level: 82,
26 | icon: FaNodeJs
27 | },
28 | {
29 | name: "Python",
30 | level: 75,
31 | icon: FaPython
32 | },
33 | {
34 | name: "Astro",
35 | level: 80,
36 | icon: SiAstro
37 | }
38 | ];
39 | ---
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | Hello, I'm John Doe
52 |
53 |
54 |
55 | A designer and developer with a passion for creating clean, functional, and beautiful digital experiences.
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
71 |
72 |
73 |
74 |
75 | 👋
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | About Me
87 |
88 |
89 |
90 |
91 |
92 | I'm a designer and developer with a passion for creating clean, functional, and beautiful digital experiences.
93 | With over 5 years of experience in the industry, I've worked on a variety of projects from small personal
94 | websites to large enterprise applications.
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
Tech Stack
105 |
106 |
107 |
108 |
109 | {[...skills, ...skills, ...skills].map((skill, index) => (
110 |
135 | ))}
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
Get in Touch
147 |
148 | I'm always open to new opportunities and collaborations. If you'd like to work together or just say hello,
149 | feel free to reach out.
150 |
151 |
152 |
156 |
157 |
158 |
159 | Say Hello
160 |
161 |
162 |
163 |
164 |
165 |
316 |
317 |
427 |
428 |
--------------------------------------------------------------------------------
/src/layouts/styles/markdown.css:
--------------------------------------------------------------------------------
1 | /* Article entrance animation */
2 | article {
3 | opacity: 1;
4 | transform: translateY(0);
5 | }
6 |
7 | article.article-entering {
8 | animation: article-fade-in 0.8s ease forwards;
9 | }
10 |
11 | @keyframes article-fade-in {
12 | from {
13 | opacity: 0;
14 | transform: translateY(20px);
15 | }
16 | to {
17 | opacity: 1;
18 | transform: translateY(0);
19 | }
20 | }
21 |
22 | /* Hero image hover effect */
23 | article img {
24 | transition: transform 0.7s cubic-bezier(0.33, 1, 0.68, 1);
25 | }
26 |
27 | /* Heading animations */
28 | article .heading-animated {
29 | opacity: 0;
30 | transform: translateY(10px);
31 | transition: opacity 0.5s ease, transform 0.5s ease;
32 | }
33 |
34 | article .heading-visible {
35 | opacity: 1;
36 | transform: translateY(0);
37 | }
38 |
39 | /* Navigation link hover effect */
40 | .blog-nav-link {
41 | transition: transform 0.3s ease, box-shadow 0.3s ease;
42 | }
43 |
44 | .blog-nav-link.nav-link-hover {
45 | transform: translateY(-2px);
46 | box-shadow: 0 10px 20px -10px rgba(0, 0, 0, 0.1);
47 | }
48 |
49 | /* Ensure dark mode compatibility */
50 | :global(.dark) .blog-nav-link.nav-link-hover {
51 | box-shadow: 0 10px 20px -10px rgba(0, 0, 0, 0.3);
52 | }
53 |
54 | /* Enhanced Markdown Content Styling */
55 | .markdown-content {
56 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
57 | line-height: 1.7;
58 | color: #374151;
59 | }
60 |
61 | .dark .markdown-content {
62 | color: #e5e7eb;
63 | }
64 |
65 | /* Headings */
66 | .markdown-content h1 {
67 | font-size: 2.5rem;
68 | font-weight: 800;
69 | margin-top: 2.5rem;
70 | margin-bottom: 1.5rem;
71 | line-height: 1.2;
72 | color: #111827;
73 | border-bottom: 1px solid #e5e7eb;
74 | padding-bottom: 0.5rem;
75 | }
76 |
77 | .dark .markdown-content h1 {
78 | color: #f9fafb;
79 | border-bottom-color: #374151;
80 | }
81 |
82 | .markdown-content h2 {
83 | font-size: 2rem;
84 | font-weight: 700;
85 | margin-top: 2.5rem;
86 | margin-bottom: 1rem;
87 | line-height: 1.3;
88 | color: #111827;
89 | border-bottom: 1px solid #e5e7eb;
90 | padding-bottom: 0.5rem;
91 | }
92 |
93 | .dark .markdown-content h2 {
94 | color: #f9fafb;
95 | border-bottom-color: #374151;
96 | }
97 |
98 | .markdown-content h3 {
99 | font-size: 1.5rem;
100 | font-weight: 600;
101 | margin-top: 2rem;
102 | margin-bottom: 1rem;
103 | line-height: 1.4;
104 | color: #111827;
105 | }
106 |
107 | .dark .markdown-content h3 {
108 | color: #f9fafb;
109 | }
110 |
111 | .markdown-content h4 {
112 | font-size: 1.25rem;
113 | font-weight: 600;
114 | margin-top: 1.5rem;
115 | margin-bottom: 0.75rem;
116 | line-height: 1.5;
117 | color: #111827;
118 | }
119 |
120 | .dark .markdown-content h4 {
121 | color: #f9fafb;
122 | }
123 |
124 | .markdown-content h5 {
125 | font-size: 1.125rem;
126 | font-weight: 600;
127 | margin-top: 1.5rem;
128 | margin-bottom: 0.75rem;
129 | line-height: 1.5;
130 | color: #111827;
131 | }
132 |
133 | .dark .markdown-content h5 {
134 | color: #f9fafb;
135 | }
136 |
137 | .markdown-content h6 {
138 | font-size: 1rem;
139 | font-weight: 600;
140 | margin-top: 1.5rem;
141 | margin-bottom: 0.75rem;
142 | line-height: 1.5;
143 | color: #111827;
144 | }
145 |
146 | .dark .markdown-content h6 {
147 | color: #f9fafb;
148 | }
149 |
150 | /* Paragraphs */
151 | .markdown-content p {
152 | margin-top: 1.25rem;
153 | margin-bottom: 1.25rem;
154 | }
155 |
156 | /* Links */
157 | .markdown-content a {
158 | color: #2563eb;
159 | text-decoration: none;
160 | border-bottom: 1px solid transparent;
161 | transition: border-color 0.2s ease, color 0.2s ease;
162 | }
163 |
164 | .markdown-content a:hover {
165 | color: #1d4ed8;
166 | border-bottom-color: #1d4ed8;
167 | }
168 |
169 | .dark .markdown-content a {
170 | color: #3b82f6;
171 | }
172 |
173 | .dark .markdown-content a:hover {
174 | color: #60a5fa;
175 | border-bottom-color: #60a5fa;
176 | }
177 |
178 | /* Bold text styling - enhanced */
179 | .markdown-content strong {
180 | font-weight: 700;
181 | color: #0f766e;
182 | background: linear-gradient(to bottom, transparent 60%, rgba(20, 184, 166, 0.2) 40%);
183 | padding: 0 0.2em;
184 | border-radius: 0.2em;
185 | }
186 |
187 | .dark .markdown-content strong {
188 | color: #14b8a6;
189 | background: linear-gradient(to bottom, transparent 60%, rgba(20, 184, 166, 0.15) 40%);
190 | }
191 |
192 | /* Lists */
193 | .markdown-content ul,
194 | .markdown-content ol {
195 | margin-top: 1rem;
196 | margin-bottom: 1rem;
197 | padding-left: 1.5rem;
198 | }
199 |
200 | .markdown-content ul {
201 | list-style-type: disc;
202 | }
203 |
204 | .markdown-content ol {
205 | list-style-type: decimal;
206 | }
207 |
208 | .markdown-content li {
209 | margin-top: 0.5rem;
210 | margin-bottom: 0.5rem;
211 | }
212 |
213 | .markdown-content li > ul,
214 | .markdown-content li > ol {
215 | margin-top: 0.25rem;
216 | margin-bottom: 0.25rem;
217 | }
218 |
219 | /* Blockquotes */
220 | .markdown-content blockquote {
221 | border-left: 4px solid #3b82f6;
222 | padding: 1rem 1.5rem;
223 | margin: 1.5rem 0;
224 | background-color: #f3f4f6;
225 | border-radius: 0.375rem;
226 | font-style: italic;
227 | position: relative;
228 | overflow: hidden;
229 | }
230 |
231 | .dark .markdown-content blockquote {
232 | background-color: #1f2937;
233 | border-left-color: #60a5fa;
234 | }
235 |
236 | .markdown-content blockquote p {
237 | margin-top: 0.5rem;
238 | margin-bottom: 0.5rem;
239 | }
240 |
241 | .markdown-content blockquote .quote-icon {
242 | position: absolute;
243 | top: 0.5rem;
244 | right: 0.5rem;
245 | opacity: 0.1;
246 | color: #3b82f6;
247 | }
248 |
249 | .dark .markdown-content blockquote .quote-icon {
250 | color: #60a5fa;
251 | }
252 |
253 | /* Code blocks */
254 | .markdown-content pre {
255 | margin: 1.5rem 0;
256 | padding: 1rem;
257 | background-color: #1e293b !important;
258 | border-radius: 0.5rem;
259 | overflow-x: auto;
260 | position: relative;
261 | box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
262 | }
263 |
264 | /* Dark mode code blocks - ensure consistency */
265 | .dark .markdown-content pre {
266 | background-color: #1e293b !important;
267 | }
268 |
269 | .markdown-content pre code {
270 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
271 | font-size: 0.875rem;
272 | line-height: 1.7;
273 | color: #e5e7eb !important;
274 | background-color: transparent !important;
275 | padding: 0;
276 | border-radius: 0;
277 | display: block;
278 | }
279 |
280 | .dark .markdown-content pre code {
281 | color: #e5e7eb !important;
282 | background-color: transparent !important;
283 | }
284 |
285 | .markdown-content pre.with-line-numbers {
286 | padding-left: 3.5rem;
287 | }
288 |
289 | .markdown-content pre code {
290 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
291 | font-size: 0.875rem;
292 | line-height: 1.7;
293 | color: #e5e7eb;
294 | background-color: transparent;
295 | padding: 0;
296 | border-radius: 0;
297 | display: block;
298 | }
299 |
300 | .markdown-content .line-numbers {
301 | position: absolute;
302 | top: 1rem;
303 | left: 0;
304 | width: 2.5rem;
305 | text-align: right;
306 | padding-right: 0.75rem;
307 | color: #6b7280;
308 | user-select: none;
309 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
310 | font-size: 0.875rem;
311 | line-height: 1.7;
312 | border-right: 1px solid #4b5563;
313 | height: calc(100% - 2rem);
314 | overflow: hidden;
315 | }
316 |
317 | .markdown-content .line-numbers span {
318 | display: block;
319 | height: 1.7em;
320 | }
321 |
322 | .markdown-content .copy-code-button {
323 | position: absolute;
324 | top: 0.25rem;
325 | right: 0.25rem;
326 | background-color: #4b5563;
327 | color: #e5e7eb;
328 | border: none;
329 | border-radius: 0.25rem;
330 | padding: 0.15rem;
331 | cursor: pointer;
332 | opacity: 0.6;
333 | transition: opacity 0.2s ease;
334 | z-index: 10;
335 | width: 1.25rem;
336 | height: 1.25rem;
337 | display: flex;
338 | align-items: center;
339 | justify-content: center;
340 | }
341 |
342 | .markdown-content .copy-code-button:hover {
343 | opacity: 1;
344 | background-color: #6b7280;
345 | }
346 |
347 | .markdown-content .copy-code-button svg {
348 | width: 0.875rem;
349 | height: 0.875rem;
350 | }
351 |
352 | /* Language label */
353 | .markdown-content .language-label {
354 | position: absolute;
355 | top: 0;
356 | right: 2.5rem;
357 | background-color: #4b5563;
358 | color: #e5e7eb;
359 | font-size: 0.65rem;
360 | padding: 0.125rem 0.375rem;
361 | border-bottom-left-radius: 0.25rem;
362 | border-bottom-right-radius: 0.25rem;
363 | text-transform: uppercase;
364 | font-weight: 600;
365 | letter-spacing: 0.05em;
366 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
367 | opacity: 0.8;
368 | transition: opacity 0.2s ease;
369 | z-index: 5;
370 | }
371 |
372 | .markdown-content pre:hover .language-label {
373 | opacity: 1;
374 | }
375 |
376 | /* Language badge at bottom right */
377 | .markdown-content .language-badge {
378 | position: absolute;
379 | bottom: 0.5rem;
380 | right: 0.5rem;
381 | font-size: 0.7rem;
382 | padding: 0.1rem 0.3rem;
383 | background-color: rgba(75, 85, 99, 0.7);
384 | color: #e5e7eb;
385 | border-radius: 0.25rem;
386 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
387 | opacity: 0.8;
388 | transition: opacity 0.2s ease;
389 | z-index: 10;
390 | }
391 |
392 | .markdown-content pre:hover .language-badge {
393 | opacity: 1;
394 | }
395 |
396 | /* Inline code */
397 | .markdown-content code:not(pre code) {
398 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
399 | font-size: 0.875em;
400 | color: #ef4444;
401 | background-color: #f3f4f6;
402 | padding: 0.2em 0.4em;
403 | border-radius: 0.25rem;
404 | white-space: nowrap;
405 | }
406 |
407 | .dark .markdown-content code:not(pre code) {
408 | color: #f87171;
409 | background-color: #1f2937;
410 | }
411 |
412 | /* Tables */
413 | .markdown-content .table-container {
414 | overflow-x: auto;
415 | margin: 1.5rem 0;
416 | border-radius: 0.5rem;
417 | box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
418 | }
419 |
420 | .markdown-content table {
421 | width: 100%;
422 | border-collapse: collapse;
423 | text-align: left;
424 | font-size: 0.875rem;
425 | }
426 |
427 | .markdown-content table th {
428 | background-color: #f3f4f6;
429 | color: #111827;
430 | font-weight: 600;
431 | padding: 0.75rem 1rem;
432 | border-bottom: 2px solid #e5e7eb;
433 | }
434 |
435 | .dark .markdown-content table th {
436 | background-color: #1f2937;
437 | color: #f9fafb;
438 | border-bottom-color: #374151;
439 | }
440 |
441 | .markdown-content table td {
442 | padding: 0.75rem 1rem;
443 | border-bottom: 1px solid #e5e7eb;
444 | }
445 |
446 | .dark .markdown-content table td {
447 | border-bottom-color: #374151;
448 | }
449 |
450 | .markdown-content table tr.even-row {
451 | background-color: #f9fafb;
452 | }
453 |
454 | .dark .markdown-content table tr.even-row {
455 | background-color: #111827;
456 | }
457 |
458 | .markdown-content table tr.odd-row {
459 | background-color: #ffffff;
460 | }
461 |
462 | .dark .markdown-content table tr.odd-row {
463 | background-color: #1f2937;
464 | }
465 |
466 | .markdown-content table tr:last-child td {
467 | border-bottom: none;
468 | }
469 |
470 | /* Images */
471 | .markdown-content img {
472 | max-width: 100%;
473 | height: auto;
474 | border-radius: 0.5rem;
475 | margin: 1.5rem 0;
476 | box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
477 | }
478 |
479 | /* Horizontal rule */
480 | .markdown-content hr {
481 | border: 0;
482 | height: 1px;
483 | background-color: #e5e7eb;
484 | margin: 2rem 0;
485 | }
486 |
487 | .dark .markdown-content hr {
488 | background-color: #374151;
489 | }
490 |
491 | /* Task lists */
492 | .markdown-content ul li[data-task-list-item] {
493 | list-style-type: none;
494 | position: relative;
495 | padding-left: 1.5rem;
496 | }
497 |
498 | .markdown-content ul li[data-task-list-item]::before {
499 | content: '';
500 | position: absolute;
501 | left: 0;
502 | top: 0.25rem;
503 | width: 1rem;
504 | height: 1rem;
505 | border: 1px solid #9ca3af;
506 | border-radius: 0.25rem;
507 | }
508 |
509 | .markdown-content ul li[data-task-list-item][data-checked]::before {
510 | background-color: #3b82f6;
511 | border-color: #3b82f6;
512 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23ffffff'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 13l4 4L19 7'%3E%3C/path%3E%3C/svg%3E");
513 | background-size: 0.75rem;
514 | background-position: center;
515 | background-repeat: no-repeat;
516 | }
517 |
518 | /* Footnotes */
519 | .markdown-content .footnotes {
520 | margin-top: 2rem;
521 | padding-top: 1rem;
522 | border-top: 1px solid #e5e7eb;
523 | font-size: 0.875rem;
524 | }
525 |
526 | .dark .markdown-content .footnotes {
527 | border-top-color: #374151;
528 | }
529 |
530 | .markdown-content .footnotes ol {
531 | padding-left: 1rem;
532 | }
533 |
534 | .markdown-content .footnotes li {
535 | margin-bottom: 0.5rem;
536 | }
537 |
538 | .markdown-content .footnote-backref {
539 | font-size: 0.75rem;
540 | vertical-align: super;
541 | }
542 |
543 | /* Definition lists */
544 | .markdown-content dl {
545 | margin: 1.5rem 0;
546 | }
547 |
548 | .markdown-content dt {
549 | font-weight: 600;
550 | color: #111827;
551 | margin-top: 1rem;
552 | }
553 |
554 | .dark .markdown-content dt {
555 | color: #f9fafb;
556 | }
557 |
558 | .markdown-content dd {
559 | margin-left: 1.5rem;
560 | margin-bottom: 1rem;
561 | }
562 |
563 | /* Callouts and admonitions */
564 | .markdown-content .callout {
565 | margin: 1.5rem 0;
566 | padding: 1rem;
567 | border-radius: 0.5rem;
568 | border-left: 4px solid;
569 | background-color: #f3f4f6;
570 | }
571 |
572 | .dark .markdown-content .callout {
573 | background-color: #1f2937;
574 | }
575 |
576 | .markdown-content .callout.info {
577 | border-left-color: #3b82f6;
578 | }
579 |
580 | .markdown-content .callout.warning {
581 | border-left-color: #f59e0b;
582 | background-color: rgba(245, 158, 11, 0.1);
583 | }
584 |
585 | .dark .markdown-content .callout.warning {
586 | background-color: rgba(245, 158, 11, 0.05);
587 | }
588 |
589 | .markdown-content .callout.danger {
590 | border-left-color: #ef4444;
591 | background-color: rgba(239, 68, 68, 0.1);
592 | }
593 |
594 | .dark .markdown-content .callout.danger {
595 | background-color: rgba(239, 68, 68, 0.05);
596 | }
597 |
598 | .markdown-content .callout.tip {
599 | border-left-color: #10b981;
600 | background-color: rgba(16, 185, 129, 0.1);
601 | }
602 |
603 | .dark .markdown-content .callout.tip {
604 | background-color: rgba(16, 185, 129, 0.05);
605 | }
606 |
607 | /* Code syntax highlighting - Light theme */
608 | .markdown-content .token.comment,
609 | .markdown-content .token.prolog,
610 | .markdown-content .token.doctype,
611 | .markdown-content .token.cdata {
612 | color: #6b7280;
613 | }
614 |
615 | .markdown-content .token.punctuation {
616 | color: #6b7280;
617 | }
618 |
619 | .markdown-content .token.namespace {
620 | opacity: 0.7;
621 | }
622 |
623 | .markdown-content .token.property,
624 | .markdown-content .token.tag,
625 | .markdown-content .token.boolean,
626 | .markdown-content .token.number,
627 | .markdown-content .token.constant,
628 | .markdown-content .token.symbol {
629 | color: #ef4444;
630 | }
631 |
632 | .markdown-content .token.selector,
633 | .markdown-content .token.attr-name,
634 | .markdown-content .token.string,
635 | .markdown-content .token.char,
636 | .markdown-content .token.builtin {
637 | color: #10b981;
638 | }
639 |
640 | .markdown-content .token.operator,
641 | .markdown-content .token.entity,
642 | .markdown-content .token.url,
643 | .markdown-content .language-css .token.string,
644 | .markdown-content .style .token.string {
645 | color: #9333ea;
646 | }
647 |
648 | .markdown-content .token.atrule,
649 | .markdown-content .token.attr-value,
650 | .markdown-content .token.keyword {
651 | color: #3b82f6;
652 | }
653 |
654 | .markdown-content .token.function,
655 | .markdown-content .token.class-name {
656 | color: #f59e0b;
657 | }
658 |
659 | .markdown-content .token.regex,
660 | .markdown-content .token.important,
661 | .markdown-content .token.variable {
662 | color: #ec4899;
663 | }
664 |
665 | .markdown-content .token.important,
666 | .markdown-content .token.bold {
667 | font-weight: bold;
668 | }
669 |
670 | .markdown-content .token.italic {
671 | font-style: italic;
672 | }
673 |
674 | .markdown-content .token.entity {
675 | cursor: help;
676 | }
677 |
678 | /* Responsive adjustments */
679 | @media (max-width: 640px) {
680 | .markdown-content h1 {
681 | font-size: 2rem;
682 | }
683 |
684 | .markdown-content h2 {
685 | font-size: 1.5rem;
686 | }
687 |
688 | .markdown-content h3 {
689 | font-size: 1.25rem;
690 | }
691 |
692 | .markdown-content pre {
693 | padding: 0.75rem;
694 | }
695 |
696 | .markdown-content pre.with-line-numbers {
697 | padding-left: 3rem;
698 | }
699 |
700 | .markdown-content .line-numbers {
701 | width: 2rem;
702 | }
703 |
704 | .markdown-content blockquote {
705 | padding: 0.75rem 1rem;
706 | }
707 | }
708 |
709 | /* Print styles */
710 | @media print {
711 | .markdown-content {
712 | font-size: 12pt;
713 | }
714 |
715 | .markdown-content pre,
716 | .markdown-content code {
717 | font-size: 10pt;
718 | }
719 |
720 | .markdown-content a {
721 | color: #000 !important;
722 | text-decoration: underline;
723 | }
724 |
725 | .markdown-content blockquote {
726 | border-left: 2pt solid #000;
727 | padding: 0.5cm 1cm;
728 | background: none !important;
729 | }
730 |
731 | .markdown-content img {
732 | max-width: 100% !important;
733 | page-break-inside: avoid;
734 | }
735 |
736 | .markdown-content h2,
737 | .markdown-content h3,
738 | .markdown-content h4 {
739 | page-break-after: avoid;
740 | }
741 |
742 | .markdown-content p,
743 | .markdown-content h2,
744 | .markdown-content h3 {
745 | orphans: 3;
746 | widows: 3;
747 | }
748 | }
749 |
750 | /* Additional elements */
751 | .markdown-content details {
752 | margin: 1.5rem 0;
753 | padding: 0.5rem 1rem;
754 | background-color: #f3f4f6;
755 | border-radius: 0.5rem;
756 | border: 1px solid #e5e7eb;
757 | }
758 |
759 | .dark .markdown-content details {
760 | background-color: #1f2937;
761 | border-color: #374151;
762 | }
763 |
764 | .markdown-content details summary {
765 | font-weight: 600;
766 | cursor: pointer;
767 | padding: 0.5rem 0;
768 | }
769 |
770 | .markdown-content details[open] summary {
771 | margin-bottom: 0.5rem;
772 | border-bottom: 1px solid #e5e7eb;
773 | }
774 |
775 | .dark .markdown-content details[open] summary {
776 | border-bottom-color: #374151;
777 | }
778 |
779 | /* Keyboard shortcuts */
780 | .markdown-content kbd {
781 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
782 | font-size: 0.8em;
783 | padding: 0.2em 0.4em;
784 | margin: 0 0.1em;
785 | background-color: #f3f4f6;
786 | border: 1px solid #d1d5db;
787 | border-radius: 0.25rem;
788 | box-shadow: 0 1px 0 #d1d5db;
789 | }
790 |
791 | .dark .markdown-content kbd {
792 | background-color: #1f2937;
793 | border-color: #4b5563;
794 | box-shadow: 0 1px 0 #4b5563;
795 | }
796 |
797 | /* Abbreviations */
798 | .markdown-content abbr {
799 | cursor: help;
800 | text-decoration: underline dotted;
801 | }
802 |
803 | /* Highlight text */
804 | .markdown-content mark {
805 | background-color: #fef3c7;
806 | color: #92400e;
807 | padding: 0.1em 0.2em;
808 | border-radius: 0.25rem;
809 | }
810 |
811 | .dark .markdown-content mark {
812 | background-color: rgba(254, 243, 199, 0.2);
813 | color: #fbbf24;
814 | }
815 |
816 | /* Subscript and superscript */
817 | .markdown-content sub,
818 | .markdown-content sup {
819 | font-size: 0.75em;
820 | line-height: 0;
821 | position: relative;
822 | vertical-align: baseline;
823 | }
824 |
825 | .markdown-content sup {
826 | top: -0.5em;
827 | }
828 |
829 | .markdown-content sub {
830 | bottom: -0.25em;
831 | }
832 |
833 | /* Diagrams and charts */
834 | .markdown-content .mermaid {
835 | margin: 1.5rem 0;
836 | text-align: center;
837 | }
838 |
839 | /* Math equations */
840 | .markdown-content .math {
841 | overflow-x: auto;
842 | margin: 1.5rem 0;
843 | }
844 |
845 | /* Embedded content */
846 | .markdown-content iframe {
847 | max-width: 100%;
848 | margin: 1.5rem 0;
849 | border-radius: 0.5rem;
850 | box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
851 | }
--------------------------------------------------------------------------------