├── .env.example ├── .gitignore ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── README.md ├── astro.config.mjs ├── package.json ├── pnpm-lock.yaml ├── public ├── favicon.svg ├── placeholder-about.jpg ├── placeholder-hero.jpg └── placeholder-social.jpg ├── remark-reading-time.mjs ├── renovate.json ├── src ├── components │ ├── BaseHead.astro │ ├── DateTime.tsx │ ├── Footer.tsx │ ├── Header.tsx │ ├── HeaderLink.tsx │ ├── PostDates.tsx │ ├── PostsList.tsx │ ├── Tag.tsx │ └── Tile.tsx ├── config.ts ├── env.d.ts ├── layouts │ └── BlogPost.astro ├── pages │ ├── about.md │ ├── blog.astro │ ├── blog │ │ ├── first-post.md │ │ ├── markdown-style-guide.md │ │ ├── second-post.md │ │ ├── third-post.md │ │ └── using-mdx.mdx │ ├── index.astro │ ├── rss.xml.ts │ ├── tags.astro │ └── tags │ │ └── [tag].astro └── styles │ ├── global.css │ └── shiki.css ├── tailwind.config.cjs └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | PUBLIC_SITE_TITLE=The Website 2 | PUBLIC_SITE_DESCRIPTION=Life, the Universe and Everything 3 | PUBLIC_OWNER=John Doe 4 | PUBLIC_START_YEAR=2022 5 | PUBLIC_MAINTENANCE_NOTICE= 6 | PUBLIC_LICENSE_NOTICE=All rights reserved. 7 | PUBLIC_TWITTER=strodotbuild 8 | PUBLIC_GITHUB=withastro/astro 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,jetbrains+all,react,node 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,jetbrains+all,react,node 3 | 4 | ### JetBrains+all ### 5 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 6 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 7 | 8 | # User-specific stuff 9 | .idea/**/workspace.xml 10 | .idea/**/tasks.xml 11 | .idea/**/usage.statistics.xml 12 | .idea/**/dictionaries 13 | .idea/**/shelf 14 | 15 | # AWS User-specific 16 | .idea/**/aws.xml 17 | 18 | # Generated files 19 | .idea/**/contentModel.xml 20 | 21 | # Sensitive or high-churn files 22 | .idea/**/dataSources/ 23 | .idea/**/dataSources.ids 24 | .idea/**/dataSources.local.xml 25 | .idea/**/sqlDataSources.xml 26 | .idea/**/dynamic.xml 27 | .idea/**/uiDesigner.xml 28 | .idea/**/dbnavigator.xml 29 | 30 | # Gradle 31 | .idea/**/gradle.xml 32 | .idea/**/libraries 33 | 34 | # Gradle and Maven with auto-import 35 | # When using Gradle or Maven with auto-import, you should exclude module files, 36 | # since they will be recreated, and may cause churn. Uncomment if using 37 | # auto-import. 38 | # .idea/artifacts 39 | # .idea/compiler.xml 40 | # .idea/jarRepositories.xml 41 | # .idea/modules.xml 42 | # .idea/*.iml 43 | # .idea/modules 44 | # *.iml 45 | # *.ipr 46 | 47 | # CMake 48 | cmake-build-*/ 49 | 50 | # Mongo Explorer plugin 51 | .idea/**/mongoSettings.xml 52 | 53 | # File-based project format 54 | *.iws 55 | 56 | # IntelliJ 57 | out/ 58 | 59 | # mpeltonen/sbt-idea plugin 60 | .idea_modules/ 61 | 62 | # JIRA plugin 63 | atlassian-ide-plugin.xml 64 | 65 | # Cursive Clojure plugin 66 | .idea/replstate.xml 67 | 68 | # SonarLint plugin 69 | .idea/sonarlint/ 70 | 71 | # Crashlytics plugin (for Android Studio and IntelliJ) 72 | com_crashlytics_export_strings.xml 73 | crashlytics.properties 74 | crashlytics-build.properties 75 | fabric.properties 76 | 77 | # Editor-based Rest Client 78 | .idea/httpRequests 79 | 80 | # Android studio 3.1+ serialized cache file 81 | .idea/caches/build_file_checksums.ser 82 | 83 | ### JetBrains+all Patch ### 84 | # Ignore everything but code style settings and run configurations 85 | # that are supposed to be shared within teams. 86 | 87 | .idea/* 88 | 89 | !.idea/codeStyles 90 | !.idea/runConfigurations 91 | 92 | ### Node ### 93 | # Logs 94 | logs 95 | *.log 96 | npm-debug.log* 97 | yarn-debug.log* 98 | yarn-error.log* 99 | lerna-debug.log* 100 | .pnpm-debug.log* 101 | 102 | # Diagnostic reports (https://nodejs.org/api/report.html) 103 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 104 | 105 | # Runtime data 106 | pids 107 | *.pid 108 | *.seed 109 | *.pid.lock 110 | 111 | # Directory for instrumented libs generated by jscoverage/JSCover 112 | lib-cov 113 | 114 | # Coverage directory used by tools like istanbul 115 | coverage 116 | *.lcov 117 | 118 | # nyc test coverage 119 | .nyc_output 120 | 121 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 122 | .grunt 123 | 124 | # Bower dependency directory (https://bower.io/) 125 | bower_components 126 | 127 | # node-waf configuration 128 | .lock-wscript 129 | 130 | # Compiled binary addons (https://nodejs.org/api/addons.html) 131 | build/Release 132 | 133 | # Dependency directories 134 | node_modules/ 135 | jspm_packages/ 136 | 137 | # Snowpack dependency directory (https://snowpack.dev/) 138 | web_modules/ 139 | 140 | # TypeScript cache 141 | *.tsbuildinfo 142 | 143 | # Optional npm cache directory 144 | .npm 145 | 146 | # Optional eslint cache 147 | .eslintcache 148 | 149 | # Optional stylelint cache 150 | .stylelintcache 151 | 152 | # Microbundle cache 153 | .rpt2_cache/ 154 | .rts2_cache_cjs/ 155 | .rts2_cache_es/ 156 | .rts2_cache_umd/ 157 | 158 | # Optional REPL history 159 | .node_repl_history 160 | 161 | # Output of 'npm pack' 162 | *.tgz 163 | 164 | # Yarn Integrity file 165 | .yarn-integrity 166 | 167 | # dotenv environment variable files 168 | .env 169 | .env.development.local 170 | .env.test.local 171 | .env.production.local 172 | .env.local 173 | 174 | # parcel-bundler cache (https://parceljs.org/) 175 | .cache 176 | .parcel-cache 177 | 178 | # Next.js build output 179 | .next 180 | out 181 | 182 | # Nuxt.js build / generate output 183 | .nuxt 184 | dist 185 | 186 | # Gatsby files 187 | .cache/ 188 | # Comment in the public line in if your project uses Gatsby and not Next.js 189 | # https://nextjs.org/blog/next-9-1#public-directory-support 190 | # public 191 | 192 | # vuepress build output 193 | .vuepress/dist 194 | 195 | # vuepress v2.x temp and cache directory 196 | .temp 197 | 198 | # Docusaurus cache and generated files 199 | .docusaurus 200 | 201 | # Serverless directories 202 | .serverless/ 203 | 204 | # FuseBox cache 205 | .fusebox/ 206 | 207 | # DynamoDB Local files 208 | .dynamodb/ 209 | 210 | # TernJS port file 211 | .tern-port 212 | 213 | # Stores VSCode versions used for testing VSCode extensions 214 | .vscode-test 215 | 216 | # yarn v2 217 | .yarn/cache 218 | .yarn/unplugged 219 | .yarn/build-state.yml 220 | .yarn/install-state.gz 221 | .pnp.* 222 | 223 | ### Node Patch ### 224 | # Serverless Webpack directories 225 | .webpack/ 226 | 227 | # Optional stylelint cache 228 | 229 | # SvelteKit build / generate output 230 | .svelte-kit 231 | 232 | ### react ### 233 | .DS_* 234 | **/*.backup.* 235 | **/*.back.* 236 | 237 | node_modules 238 | 239 | *.sublime* 240 | 241 | psd 242 | thumb 243 | sketch 244 | 245 | ### VisualStudioCode ### 246 | .vscode/* 247 | !.vscode/settings.json 248 | !.vscode/tasks.json 249 | !.vscode/launch.json 250 | !.vscode/extensions.json 251 | !.vscode/*.code-snippets 252 | 253 | # Local History for Visual Studio Code 254 | .history/ 255 | 256 | # Built Visual Studio Code Extensions 257 | *.vsix 258 | 259 | ### VisualStudioCode Patch ### 260 | # Ignore all local history of files 261 | .history 262 | .ionide 263 | 264 | # Support for Project snippet scope 265 | .vscode/*.code-snippets 266 | 267 | # Ignore code-workspaces 268 | *.code-workspace 269 | 270 | # End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,jetbrains+all,react,node 271 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 100 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "astro-build.astro-vscode", 4 | "bradlc.vscode-tailwindcss", 5 | "DavidAnson.vscode-markdownlint", 6 | "unifiedjs.vscode-mdx" 7 | ], 8 | "unwantedRecommendations": [] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dotenv.enableAutocloaking": true, 3 | "conventionalCommits.scopes": [ 4 | "readme", 5 | "ui", 6 | "config", 7 | "extensions", 8 | "rss", 9 | "routes", 10 | "deps" 11 | ] 12 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Astro TypeScript React Tailwind Blog 2 | 3 | ## Credit 4 | 5 | This is just a customized version of . 6 | 7 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/umstek/astro-typescript-react-tailwind/tree/main) 8 | 9 | ## Customizations 10 | 11 | + Using pnpm for dependency management 12 | + Using React for components 13 | + Using Tailwind for styles 14 | + Using `.env` files for configurable values 15 | + Add extended .gitignore file generated with gitignore.io 16 | + Add reading time support 17 | + Add tags support 18 | + Fits more content in small screens 19 | + ... 20 | 21 | ## 🧞 Commands 22 | 23 | All commands are run from the root of the project, from a terminal: 24 | 25 | | Command | Action | 26 | | :------------------ | :----------------------------------------------- | 27 | | `pnpm i` | Installs dependencies | 28 | | `pnpm dev` | Starts local dev server at `localhost:3000` | 29 | | `pnpm build` | Build your production site to `./dist/` | 30 | | `pnpm preview` | Preview your build locally, before deploying | 31 | | `pnpm astro ...` | Run CLI commands like `astro add`, `astro check` | 32 | | `pnpm astro --help` | Get help using the Astro CLI | 33 | -------------------------------------------------------------------------------- /astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'astro/config'; 2 | import mdx from '@astrojs/mdx'; 3 | import sitemap from '@astrojs/sitemap'; 4 | import react from '@astrojs/react'; 5 | import tailwind from '@astrojs/tailwind'; 6 | import shikiTwoslash from 'remark-shiki-twoslash'; 7 | 8 | import { remarkReadingTime } from './remark-reading-time.mjs'; 9 | 10 | // https://astro.build/config 11 | export default defineConfig({ 12 | site: 'https://example.com', 13 | markdown: { 14 | syntaxHighlight: false, 15 | remarkPlugins: [ 16 | remarkReadingTime, 17 | [shikiTwoslash.default || shikiTwoslash, { themes: ['github-dark', 'github-light'] }], 18 | ], 19 | extendDefaultPlugins: true, 20 | }, 21 | integrations: [mdx(), react(), tailwind({ config: { applyBaseStyles: false } }), sitemap()], 22 | }); 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@example/blog", 3 | "type": "module", 4 | "version": "0.3.0", 5 | "private": true, 6 | "scripts": { 7 | "dev": "astro dev --host --open", 8 | "start": "astro dev", 9 | "build": "astro build", 10 | "preview": "astro preview", 11 | "astro": "astro" 12 | }, 13 | "dependencies": { 14 | "@astrojs/mdx": "0.19.7", 15 | "@astrojs/react": "2.3.2", 16 | "@astrojs/rss": "2.4.4", 17 | "@astrojs/sitemap": "1.4.0", 18 | "@astrojs/tailwind": "3.1.3", 19 | "@fontsource/jetbrains-mono": "4.5.12", 20 | "@fontsource/lexend": "4.5.15", 21 | "@tailwindcss/typography": "0.5.16", 22 | "astro": "2.10.15", 23 | "mdast-util-to-string": "3.2.0", 24 | "react": "18.3.1", 25 | "react-dom": "18.3.1", 26 | "reading-time": "1.5.0", 27 | "rollup": "3.29.5", 28 | "shiki": "0.14.7", 29 | "tailwindcss": "3.4.17" 30 | }, 31 | "devDependencies": { 32 | "@types/react": "18.3.18", 33 | "@types/react-dom": "18.3.5", 34 | "@typescript/twoslash": "3.2.8", 35 | "remark-shiki-twoslash": "3.1.3" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /public/placeholder-about.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umstek/astro-typescript-react-tailwind/f71cd15e50a891be596f10f443c89898e46b0e7e/public/placeholder-about.jpg -------------------------------------------------------------------------------- /public/placeholder-hero.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umstek/astro-typescript-react-tailwind/f71cd15e50a891be596f10f443c89898e46b0e7e/public/placeholder-hero.jpg -------------------------------------------------------------------------------- /public/placeholder-social.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umstek/astro-typescript-react-tailwind/f71cd15e50a891be596f10f443c89898e46b0e7e/public/placeholder-social.jpg -------------------------------------------------------------------------------- /remark-reading-time.mjs: -------------------------------------------------------------------------------- 1 | import getReadingTime from 'reading-time'; 2 | import { toString } from 'mdast-util-to-string'; 3 | 4 | export function remarkReadingTime() { 5 | return function (tree, { data }) { 6 | const textOnPage = toString(tree); 7 | const readingTime = getReadingTime(textOnPage); 8 | // readingTime.text will give us minutes read as a friendly string, 9 | // i.e. "3 min read" 10 | data.astro.frontmatter.readingTime = readingTime; 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ], 6 | "packageRules": [ 7 | { 8 | "matchUpdateTypes": ["minor", "patch", "pin", "digest"], 9 | "automerge": true 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/components/BaseHead.astro: -------------------------------------------------------------------------------- 1 | --- 2 | // Import the global.css file here so that it is included on 3 | // all pages through the use of the component. 4 | import '../styles/global.css'; 5 | 6 | export interface Props { 7 | title: string; 8 | description: string; 9 | image?: string; 10 | } 11 | 12 | const { title, description, image = '/placeholder-social.jpg' } = Astro.props; 13 | --- 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {title} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/components/DateTime.tsx: -------------------------------------------------------------------------------- 1 | function DateTime({ date }: { date: string }) { 2 | return ( 3 | 10 | ); 11 | } 12 | 13 | export default DateTime; 14 | -------------------------------------------------------------------------------- /src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { GITHUB, LICENSE_NOTICE, MAINTENANCE_NOTICE, OWNER, START_YEAR, TWITTER } from '../config'; 2 | 3 | function Footer() { 4 | return ( 5 | 27 | ); 28 | } 29 | 30 | export default Footer; 31 | -------------------------------------------------------------------------------- /src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import HeaderLink from './HeaderLink'; 2 | 3 | import { SITE_TITLE } from '../config'; 4 | 5 | function Header({ title }: { title?: string }) { 6 | return ( 7 |
8 |
9 |
10 | 11 |

12 | {title || SITE_TITLE} 13 |

14 |
15 | 20 |
21 |
22 |
23 | ); 24 | } 25 | 26 | export default Header; 27 | -------------------------------------------------------------------------------- /src/components/HeaderLink.tsx: -------------------------------------------------------------------------------- 1 | interface HeaderProps 2 | extends React.DetailedHTMLProps< 3 | React.AnchorHTMLAttributes, 4 | HTMLAnchorElement 5 | > {} 6 | 7 | function HeaderLink(props: HeaderProps) { 8 | const { href, className, children } = props; 9 | return ( 10 | 15 | {children} 16 | 17 | ); 18 | } 19 | 20 | export default HeaderLink; 21 | -------------------------------------------------------------------------------- /src/components/PostDates.tsx: -------------------------------------------------------------------------------- 1 | import DateTime from './DateTime'; 2 | 3 | const PostDates = ({ 4 | initialDraftAt, 5 | publishedAt, 6 | updatedAt, 7 | }: { 8 | initialDraftAt?: string; 9 | publishedAt?: string; 10 | updatedAt?: string; 11 | }) => { 12 | return ( 13 |
14 | {initialDraftAt && ( 15 |
16 | ✨ 17 |
18 | )} 19 | {publishedAt && ( 20 |
21 | 📅 22 |
23 | )} 24 | {updatedAt && ( 25 |
26 | 🔃 27 |
28 | )} 29 |
30 | ); 31 | }; 32 | 33 | export default PostDates; 34 | -------------------------------------------------------------------------------- /src/components/PostsList.tsx: -------------------------------------------------------------------------------- 1 | import Tile from './Tile'; 2 | 3 | function PostsList({ posts }: { posts: Record[] }) { 4 | return ( 5 |
6 |
    7 | {posts.map((post) => ( 8 |
  • 9 | 10 |
  • 11 | ))} 12 |
13 |
14 | ); 15 | } 16 | 17 | export default PostsList; 18 | -------------------------------------------------------------------------------- /src/components/Tag.tsx: -------------------------------------------------------------------------------- 1 | const Tag = ({ 2 | children, 3 | href, 4 | count, 5 | anchorClassName, 6 | ...rest 7 | }: { 8 | children: string; 9 | href?: string; 10 | count?: number; 11 | anchorClassName?: string; 12 | } & React.DetailedHTMLProps, HTMLDivElement>) => { 13 | return ( 14 |
15 | 🏷️ 16 | 17 | {children} 18 | 19 | {count && ({count})} 20 |
21 | ); 22 | }; 23 | 24 | export default Tag; 25 | -------------------------------------------------------------------------------- /src/components/Tile.tsx: -------------------------------------------------------------------------------- 1 | import DateTime from './DateTime'; 2 | import Tag from './Tag'; 3 | 4 | function Tile({ post }: { post: Record }) { 5 | return ( 6 |
7 |
8 | 9 | 10 | 11 |
12 | 15 |
16 | {post.frontmatter.description} 17 |
18 |
19 | 20 | {post.frontmatter.tags?.map((tag: string) => ( 21 | 27 | {tag} 28 | 29 | ))} 30 | 31 |
32 |
33 | ); 34 | } 35 | 36 | export default Tile; 37 | -------------------------------------------------------------------------------- /src/config.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 BASE_URL = import.meta.env.BASE_URL; 5 | export const SITE_TITLE = import.meta.env.PUBLIC_SITE_TITLE; 6 | export const SITE_DESCRIPTION = import.meta.env.PUBLIC_SITE_DESCRIPTION; 7 | export const OWNER = import.meta.env.PUBLIC_OWNER; 8 | export const START_YEAR = import.meta.env.PUBLIC_START_YEAR; 9 | export const MAINTENANCE_NOTICE = import.meta.env.PUBLIC_MAINTENANCE_NOTICE; 10 | export const LICENSE_NOTICE = import.meta.env.PUBLIC_LICENSE_NOTICE; 11 | export const TWITTER = import.meta.env.PUBLIC_TWITTER; 12 | export const GITHUB = import.meta.env.PUBLIC_GITHUB; 13 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/layouts/BlogPost.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import BaseHead from '../components/BaseHead.astro'; 3 | import Header from '../components/Header'; 4 | import Footer from '../components/Footer'; 5 | import Tag from '../components/Tag'; 6 | import PostDates from '../components/PostDates'; 7 | 8 | export interface Props { 9 | content: { 10 | title: string; 11 | description: string; 12 | initialDraftAt?: string; 13 | publishedAt?: string; 14 | updatedAt?: string; 15 | heroImage?: string; 16 | tags?: string[]; 17 | readingTime: { 18 | text: string; 19 | }; 20 | }; 21 | } 22 | 23 | const { 24 | content: { 25 | title, 26 | description, 27 | initialDraftAt, 28 | publishedAt, 29 | updatedAt, 30 | heroImage, 31 | tags, 32 | readingTime: { text: minutesRead }, 33 | }, 34 | } = Astro.props; 35 | --- 36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 |
44 |
45 | { 46 | heroImage && ( 47 |
51 | ) 52 | } 53 |
56 |

{title}

57 |
58 |
59 | { 60 | tags?.map((tag) => ( 61 | 62 | {tag} 63 | 64 | )) 65 | } 66 |
67 |
68 | 73 |
☕{minutesRead}
74 |
75 |
76 |
77 | 78 |
79 |
80 |
81 |