Files for my website bwc9876.dev
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Create Blog

Ben C dda06976 1240bd8a

+638 -1
+2
.markdownlint.yml
··· 1 1 MD041: false 2 + MD013: false 3 + MD033: false
+1
package.json
··· 18 18 "@picocss/pico": "^1.5.10", 19 19 "astro": "^2.10.15", 20 20 "astro-icon": "^0.8.1", 21 + "cowsay": "^1.5.0", 21 22 "sharp": "^0.32.5" 22 23 }, 23 24 "devDependencies": {
+176
pnpm-lock.yaml
··· 23 23 astro-icon: 24 24 specifier: ^0.8.1 25 25 version: 0.8.1 26 + cowsay: 27 + specifier: ^1.5.0 28 + version: 1.5.0 26 29 sharp: 27 30 specifier: ^0.32.5 28 31 version: 0.32.5 ··· 1429 1432 string-width: 4.2.3 1430 1433 dev: false 1431 1434 1435 + /ansi-regex@3.0.1: 1436 + resolution: 1437 + { 1438 + integrity: sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw== 1439 + } 1440 + engines: { node: ">=4" } 1441 + dev: false 1442 + 1432 1443 /ansi-regex@5.0.1: 1433 1444 resolution: 1434 1445 { ··· 1780 1791 streamsearch: 1.1.0 1781 1792 dev: false 1782 1793 1794 + /camelcase@5.3.1: 1795 + resolution: 1796 + { 1797 + integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== 1798 + } 1799 + engines: { node: ">=6" } 1800 + dev: false 1801 + 1783 1802 /camelcase@6.3.0: 1784 1803 resolution: 1785 1804 { ··· 1920 1939 engines: { node: ">=6" } 1921 1940 dev: false 1922 1941 1942 + /cliui@6.0.0: 1943 + resolution: 1944 + { 1945 + integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== 1946 + } 1947 + dependencies: 1948 + string-width: 4.2.3 1949 + strip-ansi: 6.0.1 1950 + wrap-ansi: 6.2.0 1951 + dev: false 1952 + 1923 1953 /clone@1.0.4: 1924 1954 resolution: 1925 1955 { ··· 2017 2047 integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== 2018 2048 } 2019 2049 engines: { node: ">= 0.6" } 2050 + dev: false 2051 + 2052 + /cowsay@1.5.0: 2053 + resolution: 2054 + { 2055 + integrity: sha512-8Ipzr54Z8zROr/62C8f0PdhQcDusS05gKTS87xxdji8VbWefWly0k8BwGK7+VqamOrkv3eGsCkPtvlHzrhWsCA== 2056 + } 2057 + engines: { node: ">= 4" } 2058 + hasBin: true 2059 + dependencies: 2060 + get-stdin: 8.0.0 2061 + string-width: 2.1.1 2062 + strip-final-newline: 2.0.0 2063 + yargs: 15.4.1 2020 2064 dev: false 2021 2065 2022 2066 /cross-spawn@7.0.3: ··· 2096 2140 ms: 2.1.2 2097 2141 dev: false 2098 2142 2143 + /decamelize@1.2.0: 2144 + resolution: 2145 + { 2146 + integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== 2147 + } 2148 + engines: { node: ">=0.10.0" } 2149 + dev: false 2150 + 2099 2151 /decode-named-character-reference@1.0.2: 2100 2152 resolution: 2101 2153 { ··· 2703 2755 engines: { node: ">=6.9.0" } 2704 2756 dev: false 2705 2757 2758 + /get-caller-file@2.0.5: 2759 + resolution: 2760 + { 2761 + integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== 2762 + } 2763 + engines: { node: 6.* || 8.* || >= 10.* } 2764 + dev: false 2765 + 2766 + /get-stdin@8.0.0: 2767 + resolution: 2768 + { 2769 + integrity: sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg== 2770 + } 2771 + engines: { node: ">=10" } 2772 + dev: false 2773 + 2706 2774 /get-stream@6.0.1: 2707 2775 resolution: 2708 2776 { ··· 3098 3166 integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== 3099 3167 } 3100 3168 engines: { node: ">=0.10.0" } 3169 + dev: false 3170 + 3171 + /is-fullwidth-code-point@2.0.0: 3172 + resolution: 3173 + { 3174 + integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w== 3175 + } 3176 + engines: { node: ">=4" } 3101 3177 dev: false 3102 3178 3103 3179 /is-fullwidth-code-point@3.0.0: ··· 4839 4915 unist-util-visit: 4.1.2 4840 4916 dev: false 4841 4917 4918 + /require-directory@2.1.1: 4919 + resolution: 4920 + { 4921 + integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== 4922 + } 4923 + engines: { node: ">=0.10.0" } 4924 + dev: false 4925 + 4926 + /require-main-filename@2.0.0: 4927 + resolution: 4928 + { 4929 + integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== 4930 + } 4931 + dev: false 4932 + 4842 4933 /resolve-from@5.0.0: 4843 4934 resolution: 4844 4935 { ··· 5040 5131 } 5041 5132 dev: false 5042 5133 5134 + /set-blocking@2.0.0: 5135 + resolution: 5136 + { 5137 + integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== 5138 + } 5139 + dev: false 5140 + 5043 5141 /sharp@0.32.5: 5044 5142 resolution: 5045 5143 { ··· 5225 5323 queue-tick: 1.0.1 5226 5324 dev: false 5227 5325 5326 + /string-width@2.1.1: 5327 + resolution: 5328 + { 5329 + integrity: sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== 5330 + } 5331 + engines: { node: ">=4" } 5332 + dependencies: 5333 + is-fullwidth-code-point: 2.0.0 5334 + strip-ansi: 4.0.0 5335 + dev: false 5336 + 5228 5337 /string-width@4.2.3: 5229 5338 resolution: 5230 5339 { ··· 5266 5375 dependencies: 5267 5376 character-entities-html4: 2.1.0 5268 5377 character-entities-legacy: 3.0.0 5378 + dev: false 5379 + 5380 + /strip-ansi@4.0.0: 5381 + resolution: 5382 + { 5383 + integrity: sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow== 5384 + } 5385 + engines: { node: ">=4" } 5386 + dependencies: 5387 + ansi-regex: 3.0.1 5269 5388 dev: false 5270 5389 5271 5390 /strip-ansi@6.0.1: ··· 5922 6041 engines: { node: ">= 8" } 5923 6042 dev: false 5924 6043 6044 + /which-module@2.0.1: 6045 + resolution: 6046 + { 6047 + integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== 6048 + } 6049 + dev: false 6050 + 5925 6051 /which-pm-runs@1.1.0: 5926 6052 resolution: 5927 6053 { ··· 5973 6099 string-width: 5.1.2 5974 6100 dev: false 5975 6101 6102 + /wrap-ansi@6.2.0: 6103 + resolution: 6104 + { 6105 + integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== 6106 + } 6107 + engines: { node: ">=8" } 6108 + dependencies: 6109 + ansi-styles: 4.3.0 6110 + string-width: 4.2.3 6111 + strip-ansi: 6.0.1 6112 + dev: false 6113 + 5976 6114 /wrap-ansi@8.1.0: 5977 6115 resolution: 5978 6116 { ··· 5992 6130 } 5993 6131 dev: false 5994 6132 6133 + /y18n@4.0.3: 6134 + resolution: 6135 + { 6136 + integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== 6137 + } 6138 + dev: false 6139 + 5995 6140 /yallist@3.1.1: 5996 6141 resolution: 5997 6142 { ··· 6006 6151 } 6007 6152 dev: false 6008 6153 6154 + /yargs-parser@18.1.3: 6155 + resolution: 6156 + { 6157 + integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== 6158 + } 6159 + engines: { node: ">=6" } 6160 + dependencies: 6161 + camelcase: 5.3.1 6162 + decamelize: 1.2.0 6163 + dev: false 6164 + 6009 6165 /yargs-parser@21.1.1: 6010 6166 resolution: 6011 6167 { 6012 6168 integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== 6013 6169 } 6014 6170 engines: { node: ">=12" } 6171 + dev: false 6172 + 6173 + /yargs@15.4.1: 6174 + resolution: 6175 + { 6176 + integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== 6177 + } 6178 + engines: { node: ">=8" } 6179 + dependencies: 6180 + cliui: 6.0.0 6181 + decamelize: 1.2.0 6182 + find-up: 4.1.0 6183 + get-caller-file: 2.0.5 6184 + require-directory: 2.1.1 6185 + require-main-filename: 2.0.0 6186 + set-blocking: 2.0.0 6187 + string-width: 4.2.3 6188 + which-module: 2.0.1 6189 + y18n: 4.0.3 6190 + yargs-parser: 18.1.3 6015 6191 dev: false 6016 6192 6017 6193 /yocto-queue@0.1.0:
+31
src/components/blog/CowSay.astro
··· 1 + --- 2 + import * as cowsay from "cowsay"; 3 + 4 + type Props = { 5 + color?: "warn" | "info"; 6 + } & cowsay.IOptions; 7 + 8 + const { color, ...cowOptions } = Astro.props; 9 + 10 + const cowText = cowsay.say(cowOptions); 11 + --- 12 + 13 + <pre class={color}> 14 + {cowText} 15 + </pre> 16 + 17 + <style> 18 + pre { 19 + padding: 1rem; 20 + } 21 + 22 + pre.warn { 23 + color: yellow; 24 + background-color: rgb(25, 25, 0); 25 + } 26 + 27 + pre.info { 28 + color: cyan; 29 + background-color: rgb(0, 25, 25); 30 + } 31 + </style>
+11 -1
src/content/config.ts
··· 34 34 }) 35 35 }); 36 36 37 + const blogPostsCollection = defineCollection({ 38 + schema: z.object({ 39 + title: z.string(), 40 + date: z.date(), 41 + summary: z.string(), 42 + cowsay: z.string() 43 + }) 44 + }); 45 + 37 46 export const collections = { 38 - projects: projectsCollection 47 + projects: projectsCollection, 48 + posts: blogPostsCollection 39 49 };
+304
src/content/posts/hello_world.mdx
··· 1 + --- 2 + title: Welcome to the Cowsay! 3 + date: 2023-10-15 4 + summary: Welcome to the cowsay! A simple blog centered around cows. 5 + cowsay: Hello World! 6 + --- 7 + 8 + import CowSay from "@components/blog/CowSay.astro"; 9 + 10 + ## Hey there 11 + 12 + Welcome to the cowsay! A simple blog centered around cows. 13 + I decided to make this blog as a way to track my progress in learning new things. 14 + I hope you enjoy your stay! 15 + 16 + This first post is going into a bit of detail about how I made this blog. 17 + Currently the site uses Astro so I'm going to stick with that for now. 18 + 19 + ## Making The Basic Blog 20 + 21 + Astro has a wonderful feature called the [content framework](https://docs.astro.build/en/guides/content-collections/), an extremely powerful way to easily 22 + create many pages with simple markdown and some frontmatter. 23 + 24 + First thing you have to to do is create a folder called `content` in the src directory. 25 + I already had some content setup because of the projects parts of this site. 26 + 27 + Inside the content folder you place a `config.ts` which will contain the schemas 28 + for your content's frontmatter. I'll just focus on my blog posts for now. 29 + 30 + ```ts 31 + const blogPostsCollection = defineCollection({ 32 + schema: z.object({ 33 + title: z.string(), 34 + date: z.date(), 35 + summary: z.string(), 36 + cowsay: z.string() 37 + }) 38 + }); 39 + ``` 40 + 41 + This contains the metadata each blog post will need to have in order for my site 42 + to render it. That `cowsay` is a bit special. 43 + 44 + Then, we simply export an object named `collections` which Astro will then pick 45 + up and generate TS bindings for. 46 + 47 + ```ts 48 + export const collections = { 49 + posts: blogPostsCollection 50 + }; 51 + ``` 52 + 53 + Now we can get to writing some content! To do so simply make a folder with the 54 + same name as the _key_ of the collection you want to write for. In this case, `posts`. 55 + 56 + Then create a markdown file within and start writing! Here's a little excerpt 57 + of what [this page looks like](https://github.com/Bwc9876/portfolio-site/tree/main/src/content/posts/hello_world.mdx): 58 + 59 + ```md 60 + --- 61 + title: Welcome to the Cowsay! 62 + date: 2023-10-15 63 + summary: Welcome to the cowsay! A simple blog centered around cows. 64 + cowsay: Hello World! 65 + --- 66 + 67 + ## Hey there! 68 + 69 + Welcome to the cowsay! A simple blog centered around cows. 70 + I decided to make this blog as a way to track my progress in learning new things. 71 + I hope you enjoy your stay! 72 + ``` 73 + 74 + The frontmatter is the part between the `---` and `---`. This is where you put 75 + metadata for the post. The `cowsay` field is a special one that I made up. It 76 + changes what the cow says in the header of the page. I'll get to that later. 77 + 78 + Now that we have some content, we can start writing some code to render it! 79 + 80 + I start off by making a `blog` folder in the `src/pages` directory. This is where 81 + all my blog related pages will go. I then make a `index.astro` file which will 82 + be a directory of all posts on the site. 83 + 84 + ```astro 85 + --- 86 + import Layout from "@layouts/Layout.astro"; 87 + import { getCollection } from "astro:content"; 88 + 89 + const blogEntries = await getCollection("posts"); 90 + --- 91 + 92 + <Layout title="The Cowsay - Ben C's Blog"> 93 + <h1>The Cowsay - Ben C's Blog</h1> 94 + <p>Here you'll find my blog posts, most recent first</p> 95 + { 96 + blogEntries.map((p, i) => ( 97 + <> 98 + {i === 0 && <hr />} 99 + <hgroup> 100 + <h2> 101 + <a href={`/blog/posts/${p.slug}`}>{p.data.title}</a> 102 + </h2> 103 + <h3> 104 + {p.data.date.toLocaleDateString("en-us", { 105 + weekday: "long", 106 + year: "numeric", 107 + month: "short", 108 + day: "numeric" 109 + })} 110 + </h3> 111 + </hgroup> 112 + <p> 113 + {p.data.summary}&nbsp;&nbsp;<a href={`/blog/posts/${p.slug}`}>Read More</a> 114 + </p> 115 + <hr /> 116 + </> 117 + )) 118 + } 119 + </Layout> 120 + ``` 121 + 122 + ![The rendered blog index page](/src/images/blog/hello_world/index_page.png) 123 + 124 + Great! I'll probably fiddle with it in the future but it's a good start. Now we need to make a page for each post. 125 + To make my URLs look nice I'm going to create a subfolder within `blog` called `posts` and then place a `[...slug].astro` in there. 126 + This will allow me to use `getStaticPaths()` to define the paths for each post. 127 + 128 + ```astro 129 + --- 130 + import Layout from "@layouts/Layout.astro"; 131 + import { CollectionEntry, getCollection } from "astro:content"; 132 + export const getStaticPaths = async () => { 133 + const posts = await getCollection("posts"); 134 + return posts.map((entry) => ({ 135 + params: { slug: entry.slug }, 136 + props: { entry } 137 + })); 138 + }; 139 + 140 + const { entry } = Astro.props as { entry: CollectionEntry<"posts"> }; 141 + const { Content } = await entry.render(); 142 + --- 143 + 144 + <Layout title={entry.data.title} description={entry.data.summary}> 145 + <h1>{entry.data.title}</h1> 146 + <Content /> 147 + </Layout> 148 + 149 + <style is:global> 150 + img { 151 + border: solid 1px var(--text) !important; 152 + border-radius: 5px; 153 + } 154 + </style> 155 + ``` 156 + 157 + Amazing! I'll spare you the image this time since... you're... look at it. But now we have a blog post page! 158 + Now anytime I want to make a new post I just have to make a new markdown file and it'll be rendered on the site. 159 + 160 + ## An Outline 161 + 162 + Now that we have a basic blog, I want to add a few more features to it. First I want to add the ability to see all headers in a post. 163 + This should be pretty easy to do. Astro automatically parses all the headers for us and lets us access them in the `entry` object. 164 + 165 + First we need to grab the headings from when we rendered the page: 166 + 167 + ```js 168 + const { Content, headings } = await entry.render(); 169 + ``` 170 + 171 + Then we need to map those to HTML 172 + 173 + ```astro 174 + <div class="toc"> 175 + <!-- Extra div so we can make it sticky --> 176 + <div> 177 + <span>On This Page</span> 178 + <ul> 179 + { 180 + headings.map((h) => ( 181 + <li> 182 + <a href={`#${h.slug}`}>{h.text}</a> 183 + </li> 184 + )) 185 + } 186 + </ul> 187 + </div> 188 + </div> 189 + ``` 190 + 191 + Finally some simple styles and layout 192 + 193 + ```astro 194 + <style> 195 + /** Wrapper is going around everything to make the 196 + table of contents appear on the right of the page **/ 197 + div.wrapper { 198 + display: flex; 199 + flex-direction: row; 200 + gap: 4rem; 201 + } 202 + 203 + div.toc { 204 + width: 100%; 205 + } 206 + 207 + div.toc div { 208 + top: 5rem; 209 + margin-top: 1rem; 210 + position: sticky; 211 + } 212 + 213 + div.toc ul li { 214 + list-style-type: none; 215 + } 216 + </style> 217 + ``` 218 + 219 + Finally, I want to make sure on smaller screen sizes the table of contents appears at the top rather than the side so it doesn't take up too much space. 220 + 221 + ```css 222 + div.wrapper { 223 + display: flex; 224 + flex-direction: column-reverse; 225 + gap: 1rem; 226 + } 227 + 228 + @media (min-width: 1200px) { 229 + div.wrapper { 230 + flex-direction: row; 231 + gap: 4rem; 232 + } 233 + } 234 + ``` 235 + 236 + ![The rendered blog post page with a table of contents](/src/images/blog/hello_world/table_of_contents.png) 237 + 238 + ## The Cowsay 239 + 240 + Now for the fun part. I want to make a little cow that says something in the header of each post. I'm going to use the `cowsay` field in the frontmatter to do this. 241 + I'll also provide a CowSay component that will render the cow and the text. This way I can use it MDX for admonitions. 242 + 243 + First I need to make a component that will render the cow. I'm going to use the [cowsay](https://www.npmjs.com/package/cowsay) package to do this. 244 + 245 + ```astro 246 + --- 247 + import * as cowsay from "cowsay"; 248 + 249 + type Props = { 250 + color?: "warn" | "info"; 251 + } & cowsay.IOptions; 252 + 253 + const { color, ...cowOptions } = Astro.props; 254 + 255 + const cowText = cowsay.say(cowOptions); 256 + --- 257 + 258 + <pre class={color}> 259 + {cowText} 260 + </pre> 261 + 262 + <style> 263 + pre { 264 + padding: 1rem; 265 + } 266 + 267 + pre.warn { 268 + color: yellow; 269 + background-color: rgb(25, 25, 0); 270 + } 271 + 272 + pre.info { 273 + color: cyan; 274 + background-color: rgb(0, 25, 25); 275 + } 276 + </style> 277 + ``` 278 + 279 + Now I can link it up in my blog post page! 280 + 281 + ```astro 282 + <CowSay text={entry.data.cowsay} /> 283 + ``` 284 + 285 + Et voila! A cow that says something in the header of each post! I'll probably make it a bit more fancy in the future but for now it's good enough. 286 + 287 + I can also use it for admonitions in MDX! 288 + 289 + ```mdx 290 + <CowSay color="warn" e="><" text="Warning!" /> 291 + <CowSay color="info" e="^^" T="U" text="Info!" /> 292 + ``` 293 + 294 + <CowSay color="warn" e="><" text="Warning!" /> 295 + <CowSay color="info" e="^^" T="U" text="Info!" /> 296 + 297 + I'll hold off on making an error one for now. Lest the cows get too angry. 298 + 299 + ## Conclusion 300 + 301 + I'm really happy with how this blog turned out. I'm going to keep working on it and adding new features as I go. 302 + I'm also going to try and write more posts in the future. I hope you enjoyed this one! 303 + 304 + <CowSay e="^^" text="Adiós!" />
src/images/blog/hello_world/index_page.png

This is a binary file and will not be displayed.

src/images/blog/hello_world/table_of_contents.png

This is a binary file and will not be displayed.

+3
src/layouts/Layout.astro
··· 88 88 <li> 89 89 <a href="/projects">Projects</a> 90 90 </li> 91 + <li> 92 + <a href="/blog">Blog</a> 93 + </li> 91 94 </ul> 92 95 <ul aria-hidden="true" class="mobile-socials"> 93 96 <li>
+35
src/pages/blog/index.astro
··· 1 + --- 2 + import Layout from "@layouts/Layout.astro"; 3 + import { getCollection } from "astro:content"; 4 + 5 + const blogEntries = await getCollection("posts"); 6 + --- 7 + 8 + <Layout title="The Cowsay - Ben C's Blog"> 9 + <h1>The Cowsay - Ben C's Blog</h1> 10 + <p>Here you'll find my blog posts, most recent first</p> 11 + { 12 + blogEntries.map((p, i) => ( 13 + <> 14 + {i === 0 && <hr />} 15 + <hgroup> 16 + <h2> 17 + <a href={`/blog/posts/${p.slug}`}>{p.data.title}</a> 18 + </h2> 19 + <h3> 20 + {p.data.date.toLocaleDateString("en-us", { 21 + weekday: "long", 22 + year: "numeric", 23 + month: "short", 24 + day: "numeric" 25 + })} 26 + </h3> 27 + </hgroup> 28 + <p> 29 + {p.data.summary}&nbsp;&nbsp;<a href={`/blog/posts/${p.slug}`}>Read More</a> 30 + </p> 31 + <hr /> 32 + </> 33 + )) 34 + } 35 + </Layout>
+75
src/pages/blog/posts/[...slug].astro
··· 1 + --- 2 + import CowSay from "@components/blog/CowSay.astro"; 3 + import Layout from "@layouts/Layout.astro"; 4 + import { CollectionEntry, getCollection } from "astro:content"; 5 + export const getStaticPaths = async () => { 6 + const posts = await getCollection("posts"); 7 + return posts.map((entry) => ({ 8 + params: { slug: entry.slug }, 9 + props: { entry } 10 + })); 11 + }; 12 + 13 + const { entry } = Astro.props as { entry: CollectionEntry<"posts"> }; 14 + const { Content, headings } = await entry.render(); 15 + --- 16 + 17 + <Layout title={entry.data.title} description={entry.data.summary}> 18 + <h1>{entry.data.title}</h1> 19 + <CowSay text={entry.data.cowsay} /> 20 + <div class="wrapper"> 21 + <div> 22 + <Content /> 23 + </div> 24 + <div class="toc"> 25 + <div> 26 + <span>On This Page</span> 27 + <ul> 28 + { 29 + headings.map((h) => ( 30 + <li> 31 + <a href={`#${h.slug}`}>{h.text}</a> 32 + </li> 33 + )) 34 + } 35 + </ul> 36 + </div> 37 + </div> 38 + </div> 39 + </Layout> 40 + 41 + <style> 42 + div.wrapper { 43 + display: flex; 44 + flex-direction: column-reverse; 45 + gap: 1rem; 46 + } 47 + 48 + @media (min-width: 1200px) { 49 + div.wrapper { 50 + flex-direction: row; 51 + gap: 4rem; 52 + } 53 + } 54 + 55 + div.toc { 56 + width: 100%; 57 + } 58 + 59 + div.toc div { 60 + top: 5rem; 61 + margin-top: 1rem; 62 + position: sticky; 63 + } 64 + 65 + div.toc ul li { 66 + list-style-type: none; 67 + } 68 + </style> 69 + 70 + <style is:global> 71 + img { 72 + border: solid 1px var(--text) !important; 73 + border-radius: 5px; 74 + } 75 + </style>