├── public
├── blog-placeholder-1.jpg
├── blog-placeholder-2.jpg
├── blog-placeholder-3.jpg
├── blog-placeholder-4.jpg
├── blog-placeholder-5.jpg
├── fonts
│ ├── atkinson-bold.woff
│ └── atkinson-regular.woff
├── blog-placeholder-about.jpg
└── favicon.svg
├── src
├── pages
│ ├── index.astro
│ ├── rss.xml.js
│ ├── 404.astro
│ ├── [lang]
│ │ ├── blog
│ │ │ ├── [slug].astro
│ │ │ └── pages
│ │ │ │ └── [page].astro
│ │ ├── projects
│ │ │ ├── [slug].astro
│ │ │ └── pages
│ │ │ │ └── [page].astro
│ │ ├── series
│ │ │ ├── index.astro
│ │ │ └── [series]
│ │ │ │ └── [...page].astro
│ │ ├── tags
│ │ │ ├── index.astro
│ │ │ └── [tag]
│ │ │ │ └── [...page].astro
│ │ └── archive
│ │ │ └── index.astro
│ ├── en
│ │ ├── about.astro
│ │ └── index.astro
│ └── es
│ │ ├── about.astro
│ │ └── index.astro
├── content
│ ├── blog
│ │ ├── draft.md
│ │ ├── hidden-post.md
│ │ ├── en
│ │ │ └── multi-lang-test.md
│ │ ├── es
│ │ │ └── multi-lang-test.md
│ │ ├── test-tables.md
│ │ ├── using-mdx.mdx
│ │ ├── second-post.md
│ │ ├── third-post.md
│ │ ├── first-post.md
│ │ └── markdown-style-guide.md
│ ├── project
│ │ ├── es
│ │ │ ├── project-1.md
│ │ │ ├── project-4.md
│ │ │ ├── project-3.md
│ │ │ └── project-2.md
│ │ ├── project-1.md
│ │ ├── project-4.md
│ │ ├── project-3.md
│ │ └── project-2.md
│ └── config.ts
├── env.d.ts
├── components
│ ├── FormattedDate.astro
│ ├── HeaderLink.astro
│ ├── TableOfContentsHeading.astro
│ ├── SocialMediaLinks.astro
│ ├── Pagination.astro
│ ├── Footer.astro
│ ├── BackToTopBtn.astro
│ ├── blog
│ │ ├── PostPreviewA.astro
│ │ └── PostPreviewB.astro
│ ├── SelectLanguage.astro
│ ├── CopyBtn.astro
│ ├── BaseHead.astro
│ ├── ThemeToggle.astro
│ ├── TableOfContents.astro
│ ├── Search.astro
│ └── Header.astro
├── utils
│ ├── remark-reading-time.mjs
│ ├── date.ts
│ ├── series.ts
│ ├── tags.ts
│ ├── frontmatter.ts
│ ├── index.ts
│ ├── i18n.ts
│ └── post.ts
├── layouts
│ ├── Sidebar.astro
│ ├── Base.astro
│ ├── GenericPost.astro
│ ├── Project.astro
│ └── BlogPost.astro
├── styles
│ ├── search.css
│ └── global.css
├── consts.ts
└── i18n
│ ├── ui.ts
│ └── nav.ts
├── .vscode
├── extensions.json
└── launch.json
├── .gitignore
├── tsconfig.json
├── tailwind.config.mjs
├── package.json
├── astro.config.mjs
├── LICENSE
└── README.md
/public/blog-placeholder-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kirontoo/astro-theme-cody/HEAD/public/blog-placeholder-1.jpg
--------------------------------------------------------------------------------
/public/blog-placeholder-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kirontoo/astro-theme-cody/HEAD/public/blog-placeholder-2.jpg
--------------------------------------------------------------------------------
/public/blog-placeholder-3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kirontoo/astro-theme-cody/HEAD/public/blog-placeholder-3.jpg
--------------------------------------------------------------------------------
/public/blog-placeholder-4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kirontoo/astro-theme-cody/HEAD/public/blog-placeholder-4.jpg
--------------------------------------------------------------------------------
/public/blog-placeholder-5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kirontoo/astro-theme-cody/HEAD/public/blog-placeholder-5.jpg
--------------------------------------------------------------------------------
/public/fonts/atkinson-bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kirontoo/astro-theme-cody/HEAD/public/fonts/atkinson-bold.woff
--------------------------------------------------------------------------------
/public/blog-placeholder-about.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kirontoo/astro-theme-cody/HEAD/public/blog-placeholder-about.jpg
--------------------------------------------------------------------------------
/public/fonts/atkinson-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kirontoo/astro-theme-cody/HEAD/public/fonts/atkinson-regular.woff
--------------------------------------------------------------------------------
/src/pages/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { DEFAULT_LANG } from "src/consts";
3 | return Astro.redirect(`/${DEFAULT_LANG}/`);
4 | ---
5 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["astro-build.astro-vscode", "unifiedjs.vscode-mdx"],
3 | "unwantedRecommendations": []
4 | }
5 |
--------------------------------------------------------------------------------
/src/content/blog/draft.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Test Draft Post"
3 | description: "This is a test post for draft post."
4 | pubDate: 2024-05-24
5 | draft: true
6 | ---
7 |
8 |
--------------------------------------------------------------------------------
/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | declare module "@pagefind/default-ui" {
4 | declare class PagefindUI {
5 | constructor(arg: unknown);
6 | }
7 | }
8 |
9 |
--------------------------------------------------------------------------------
/src/content/blog/hidden-post.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Test Hidden Post"
3 | description: "This is a test post for hidden post."
4 | pubDate: 2024-05-24
5 | hide: true
6 | ---
7 |
8 | This post is supposed to be hidden from `/blog` but not from `/archive`.
9 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/src/content/blog/en/multi-lang-test.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Test Post in English'
3 | description: 'Lorem ipsum dolor sit amet'
4 | pubDate: 'Sep 26 2024'
5 | tags: ["multi-lang", "test"]
6 | updatedDate: 'Sep 26 2024'
7 | series: "example"
8 | ---
9 |
10 | Is in English.
11 |
--------------------------------------------------------------------------------
/src/content/blog/es/multi-lang-test.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Test Post in Spanish'
3 | description: 'Lorem ipsum dolor sit amet'
4 | pubDate: 'Sep 26 2024'
5 | tags: ["multi-lang", "test"]
6 | updatedDate: 'Sep 26 2024'
7 | series: "example"
8 | ---
9 |
10 | Es en espanol.
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # build output
2 | dist/
3 | # generated types
4 | .astro/
5 |
6 | # dependencies
7 | node_modules/
8 |
9 | # logs
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 | pnpm-debug.log*
14 |
15 | # pagefind
16 | public/pagefind/
17 |
18 | # environment variables
19 | .env
20 | .env.production
21 |
22 | # macOS-specific files
23 | .DS_Store
24 |
--------------------------------------------------------------------------------
/src/pages/rss.xml.js:
--------------------------------------------------------------------------------
1 | import rss from '@astrojs/rss';
2 | import { getCollection } from 'astro:content';
3 | import { siteConfig } from '../consts';
4 |
5 | export async function GET(context) {
6 | const posts = await getCollection('blog');
7 | return rss({
8 | title: siteConfig.title,
9 | description: siteConfig.description,
10 | site: context.site,
11 | items: posts.map((post) => ({
12 | ...post.data,
13 | link: `/blog/${post.slug}/`,
14 | })),
15 | });
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/FormattedDate.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { HTMLAttributes } from "astro/types";
3 | import { getFormattedDate } from "@/utils";
4 |
5 | type Props = HTMLAttributes<"time"> & {
6 | date: Date;
7 | dateTimeOptions?: Intl.DateTimeFormatOptions;
8 | locale?: string;
9 | };
10 |
11 | const { date, dateTimeOptions, locale, ...attrs } = Astro.props;
12 | const postDate = getFormattedDate(date, dateTimeOptions, locale);
13 | ---
14 |
15 |
16 | {postDate}
17 |
18 |
--------------------------------------------------------------------------------
/src/utils/remark-reading-time.mjs:
--------------------------------------------------------------------------------
1 | import getReadingTime from 'reading-time';
2 | import { toString } from 'mdast-util-to-string';
3 |
4 | // https://docs.astro.build/en/recipes/reading-time/#recipe
5 | export function remarkReadingTime() {
6 | return function (tree, { data }) {
7 | const textOnPage = toString(tree);
8 | const readingTime = getReadingTime(textOnPage);
9 | // readingTime.text will give us minutes read as a friendly string,
10 | // i.e. "3 min read"
11 | data.astro.frontmatter.minutesRead = readingTime.text;
12 | };
13 | }
14 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/strict",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "strictNullChecks": true,
6 | "paths": {
7 | "@/utils": [
8 | "src/utils/index.ts"
9 | ],
10 | "@/components/*": [
11 | "src/components/*.astro"
12 | ],
13 | "@/layouts/*": [
14 | "src/layouts/*.astro"
15 | ],
16 | "@/i18n/*": [
17 | "src/i18n/*.ts"
18 | ],
19 | "@/styles/*": [
20 | "src/styles/*.css"
21 | ],
22 | }
23 | },
24 | "include": [
25 | "src",
26 | "*.ts"
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/HeaderLink.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { HTMLAttributes } from 'astro/types';
3 |
4 | type Props = HTMLAttributes<'a'>;
5 |
6 | const { href, class: className, ...props } = Astro.props;
7 |
8 | const { pathname } = Astro.url;
9 | const isActive = href === pathname || href === pathname.replace(/\/$/, '');
10 | ---
11 |
12 |
13 |
14 |
15 |
16 |
28 |
--------------------------------------------------------------------------------
/src/utils/date.ts:
--------------------------------------------------------------------------------
1 | const dateOptions = {
2 | locale: "en-GB",
3 | options: {
4 | day: "numeric",
5 | month: "short",
6 | year: "numeric",
7 | }
8 | };
9 |
10 | const dateFormat = new Intl.DateTimeFormat(
11 | dateOptions.locale,
12 | dateOptions.options as Intl.DateTimeFormatOptions
13 | );
14 |
15 | export function getFormattedDate(
16 | date: string | number | Date,
17 | options?: Intl.DateTimeFormatOptions,
18 | locale?: string,
19 | ) {
20 | if (typeof options !== "undefined") {
21 | return new Date(date).toLocaleDateString(locale ?? dateOptions.locale, options);
22 | }
23 |
24 | return dateFormat.format(new Date(date));
25 | }
26 |
--------------------------------------------------------------------------------
/src/utils/series.ts:
--------------------------------------------------------------------------------
1 | import type { CollectionEntry } from "astro:content";
2 |
3 | export function getAllSeries(posts: Array>) {
4 | return posts.flatMap(({data}) => ( data.series ? [...data.series ] : [] ));
5 | }
6 |
7 | export function getUniqueSeries(posts: Array>) {
8 | return [...new Set(getAllSeries(posts))];
9 | }
10 |
11 | export function getUniqueSeriesWithCount(
12 | posts: Array>,
13 | ): Array<[string, number]> {
14 | return [
15 | ...getAllSeries(posts).reduce(
16 | (acc, t) => acc.set(t, (acc.get(t) || 0) + 1),
17 | new Map(),
18 | ),
19 | ].sort((a, b) => b[1] - a[1]);
20 | }
21 |
--------------------------------------------------------------------------------
/tailwind.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
4 | darkMode: "class",
5 | theme: {
6 | fontFamily: {
7 | 'mono': ['Space Mono', 'monospace']
8 | },
9 | extend: {
10 | colors: {
11 | bgColor: "var(--theme-bg)",
12 | textColor: "var(--theme-text)",
13 | link: "var(--theme-link)",
14 | accent: "var(--theme-accent)",
15 | "accent-2": "var(--theme-accent-2)",
16 | surface: "var(--theme-surface)",
17 | quote: "var(--theme-quote)",
18 | highlight: "var(--theme-highlight)"
19 | },
20 | }
21 | },
22 | plugins: [
23 | require('@tailwindcss/typography'),
24 | ],
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/TableOfContentsHeading.astro:
--------------------------------------------------------------------------------
1 | ---
2 | // TableOfContentsHeading.astro
3 | import type { MarkdownHeading } from "astro";
4 | const { heading } = Astro.props;
5 | ---
6 |
7 |
8 |
9 | {heading.text}
10 |
11 | {
12 | heading.subheadings.length > 0 && (
13 |
14 | {heading.subheadings.map((subheading: MarkdownHeading[]) => (
15 |
16 | ))}
17 |
18 | )
19 | }
20 |
21 |
22 |
32 |
33 |
--------------------------------------------------------------------------------
/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
--------------------------------------------------------------------------------
/src/content/project/es/project-1.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Project 1'
3 | description: 'Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci'
4 | pubDate: 'Apr 03 2023'
5 | heroImage:
6 | url: '/blog-placeholder-about.jpg'
7 | alt: 'GitHub wallpaper'
8 | platform: Web
9 | stack: ['Astro', 'JS']
10 | website: https://github.com/kirontoo/astro-theme-cody
11 | github: https://github.com/kirontoo/astro-theme-cody
12 | ---
13 |
14 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras faucibus a tortor at molestie. Sed pellentesque leo auctor, auctor lorem nec, venenatis risus. Vivamus commodo ipsum vitae orci finibus, vel porta nunc viverra. In hac habitasse platea dictumst. Nunc pretium, ligula ultricies consequat sollicitudin, enim ex ullamcorper nisl.
15 |
--------------------------------------------------------------------------------
/src/content/project/es/project-4.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Project 4'
3 | description: 'Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci'
4 | pubDate: 'Jun 08 2003'
5 | heroImage:
6 | url: '/blog-placeholder-about.jpg'
7 | alt: 'GitHub wallpaper'
8 | platform: Web
9 | stack: ['Astro', 'JS']
10 | website: https://github.com/kirontoo/astro-theme-cody
11 | github: https://github.com/kirontoo/astro-theme-cody
12 | ---
13 |
14 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras faucibus a tortor at molestie. Sed pellentesque leo auctor, auctor lorem nec, venenatis risus. Vivamus commodo ipsum vitae orci finibus, vel porta nunc viverra. In hac habitasse platea dictumst. Nunc pretium, ligula ultricies consequat sollicitudin, enim ex ullamcorper nisl.
15 |
--------------------------------------------------------------------------------
/src/content/project/project-1.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Project 1'
3 | description: 'Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci'
4 | pubDate: 'Apr 03 2023'
5 | heroImage:
6 | url: '/blog-placeholder-about.jpg'
7 | alt: 'GitHub wallpaper'
8 | platform: Web
9 | stack: ['Astro', 'JS']
10 | website: https://github.com/kirontoo/astro-theme-cody
11 | github: https://github.com/kirontoo/astro-theme-cody
12 | ---
13 |
14 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras faucibus a tortor at molestie. Sed pellentesque leo auctor, auctor lorem nec, venenatis risus. Vivamus commodo ipsum vitae orci finibus, vel porta nunc viverra. In hac habitasse platea dictumst. Nunc pretium, ligula ultricies consequat sollicitudin, enim ex ullamcorper nisl.
15 |
--------------------------------------------------------------------------------
/src/content/project/project-4.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Project 4'
3 | description: 'Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci'
4 | pubDate: 'Jun 08 2003'
5 | heroImage:
6 | url: '/blog-placeholder-about.jpg'
7 | alt: 'GitHub wallpaper'
8 | platform: Web
9 | stack: ['Astro', 'JS']
10 | website: https://github.com/kirontoo/astro-theme-cody
11 | github: https://github.com/kirontoo/astro-theme-cody
12 | ---
13 |
14 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras faucibus a tortor at molestie. Sed pellentesque leo auctor, auctor lorem nec, venenatis risus. Vivamus commodo ipsum vitae orci finibus, vel porta nunc viverra. In hac habitasse platea dictumst. Nunc pretium, ligula ultricies consequat sollicitudin, enim ex ullamcorper nisl.
15 |
--------------------------------------------------------------------------------
/src/utils/tags.ts:
--------------------------------------------------------------------------------
1 | import type { CollectionEntry } from "astro:content";
2 |
3 | // https://github.com/chrismwilliams/astro-theme-cactus/blob/a85e0e559d3f92b32e73990486c0574b2b733227/src/utils/post.ts
4 | export function getAllTags(posts: Array>) {
5 | return posts.flatMap(({data}) => ( data.tags ? [...data.tags ] : [] ));
6 | }
7 |
8 | export function getUniqueTags(posts: Array>) {
9 | return [...new Set(getAllTags(posts))];
10 | }
11 |
12 | export function getUniqueTagsWithCount(
13 | posts: Array>,
14 | ): Array<[string, number]> {
15 | return [
16 | ...getAllTags(posts).reduce(
17 | (acc, t) => acc.set(t, (acc.get(t) || 0) + 1),
18 | new Map(),
19 | ),
20 | ].sort((a, b) => b[1] - a[1]);
21 | }
22 |
--------------------------------------------------------------------------------
/src/content/project/project-3.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Project 3'
3 | description: 'Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci'
4 | pubDate: 'Mar 03 2023'
5 | heroImage:
6 | url: '/blog-placeholder-about.jpg'
7 | alt: 'GitHub wallpaper'
8 | platform: Web
9 | stack: ['Reactjs', 'Typescript', "Mongo"]
10 | website: https://github.com/kirontoo/astro-theme-cody
11 | github: https://github.com/kirontoo/astro-theme-cody
12 | order: 1
13 | ---
14 |
15 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras faucibus a tortor at molestie. Sed pellentesque leo auctor, auctor lorem nec, venenatis risus. Vivamus commodo ipsum vitae orci finibus, vel porta nunc viverra. In hac habitasse platea dictumst. Nunc pretium, ligula ultricies consequat sollicitudin, enim ex ullamcorper nisl.
16 |
--------------------------------------------------------------------------------
/src/content/project/es/project-3.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Project 3'
3 | description: 'Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci'
4 | pubDate: 'Mar 03 2023'
5 | heroImage:
6 | url: '/blog-placeholder-about.jpg'
7 | alt: 'GitHub wallpaper'
8 | platform: Web
9 | stack: ['Reactjs', 'Typescript', "Mongo"]
10 | website: https://github.com/kirontoo/astro-theme-cody
11 | github: https://github.com/kirontoo/astro-theme-cody
12 | order: 1
13 | ---
14 |
15 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras faucibus a tortor at molestie. Sed pellentesque leo auctor, auctor lorem nec, venenatis risus. Vivamus commodo ipsum vitae orci finibus, vel porta nunc viverra. In hac habitasse platea dictumst. Nunc pretium, ligula ultricies consequat sollicitudin, enim ex ullamcorper nisl.
16 |
--------------------------------------------------------------------------------
/src/components/SocialMediaLinks.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { Icon } from "astro-icon";
3 | import { SOCIAL_LINKS } from "src/consts.ts";
4 | ---
5 |
6 |
31 |
--------------------------------------------------------------------------------
/src/content/project/project-2.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Project 2'
3 | description: 'Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci'
4 | pubDate: 'Aug 13 2022'
5 | heroImage:
6 | url: '/blog-placeholder-about.jpg'
7 | alt: 'GitHub wallpaper'
8 | platform: Web
9 | stack: ['Astro', 'JS', 'tailwind'] website: https://github.com/kirontoo/astro-theme-cody
10 | github: https://github.com/kirontoo/astro-theme-cody
11 | order: 2
12 | ---
13 |
14 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras faucibus a tortor at molestie. Sed pellentesque leo auctor, auctor lorem nec, venenatis risus. Vivamus commodo ipsum vitae orci finibus, vel porta nunc viverra. In hac habitasse platea dictumst. Nunc pretium, ligula ultricies consequat sollicitudin, enim ex ullamcorper nisl.
15 |
16 | ## hello
17 |
--------------------------------------------------------------------------------
/src/content/project/es/project-2.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Project 2'
3 | description: 'Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci'
4 | pubDate: 'Aug 13 2022'
5 | heroImage:
6 | url: '/blog-placeholder-about.jpg'
7 | alt: 'GitHub wallpaper'
8 | platform: Web
9 | stack: ['Astro', 'JS', 'tailwind'] website: https://github.com/kirontoo/astro-theme-cody
10 | github: https://github.com/kirontoo/astro-theme-cody
11 | order: 2
12 | ---
13 |
14 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras faucibus a tortor at molestie. Sed pellentesque leo auctor, auctor lorem nec, venenatis risus. Vivamus commodo ipsum vitae orci finibus, vel porta nunc viverra. In hac habitasse platea dictumst. Nunc pretium, ligula ultricies consequat sollicitudin, enim ex ullamcorper nisl.
15 |
16 | ## hello
17 |
--------------------------------------------------------------------------------
/src/utils/frontmatter.ts:
--------------------------------------------------------------------------------
1 | import type { CollectionEntry } from "astro:content";
2 |
3 | // should only be used with frontmatter arrays
4 | export function getAllPostsByProperty(prop: "series" | "tags", posts: Array>) {
5 | return posts.flatMap(({ data }) => {
6 | return data[prop] ?? [];
7 | })
8 | }
9 |
10 | export function getUniqueByProperty(prop: "series" | "tags", posts: Array>) {
11 | return [...new Set(getAllPostsByProperty(prop, posts))];
12 | }
13 |
14 | export function getUniqueWithCountByProperty(
15 | prop: "series" | "tags",
16 | posts: Array>,
17 | ): Array<[string, number]> {
18 | return [
19 | ...getAllPostsByProperty(prop, posts).reduce(
20 | (acc, t) => acc.set(t, (acc.get(t) || 0) + 1),
21 | new Map(),
22 | ),
23 | ].sort((a, b) => b[1] - a[1]);
24 | }
25 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export { getFormattedDate } from "./date";
2 | export { remarkReadingTime } from "./remark-reading-time.mjs";
3 | export {
4 | getAllTags,
5 | getUniqueTags,
6 | getUniqueTagsWithCount,
7 | } from "./tags";
8 |
9 | export {
10 | getAllPosts,
11 | getPostsByTag,
12 | getPostsBySeries,
13 | sortMDByDate,
14 | sortMDByPinned,
15 | filterByLanguage,
16 | getSlugFromCollectionEntry
17 | } from "./post";
18 |
19 | export {
20 | getAllSeries,
21 | getUniqueSeries,
22 | getUniqueSeriesWithCount,
23 | } from "./series"
24 |
25 | export {
26 | getAllPostsByProperty,
27 | getUniqueByProperty,
28 | getUniqueWithCountByProperty,
29 | } from "./frontmatter"
30 |
31 | export {
32 | type SupportedLanguage,
33 | getSupportedLanguages,
34 | isValidLanguageCode,
35 | getLangFromUrl,
36 | getLangFromSlug,
37 | useNavTranslations,
38 | useUITranslations
39 | } from "./i18n"
40 |
--------------------------------------------------------------------------------
/src/layouts/Sidebar.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import BaseHead from '../components/BaseHead.astro';
3 | import Header from '../components/Header.astro'
4 | import Footer from '../components/Footer.astro';
5 | import { siteConfig } from "../consts.ts";
6 |
7 | interface Props {
8 | title?: string;
9 | description?: string;
10 | image?: string;
11 | articleDate?: string
12 | }
13 |
14 | const {
15 | title = siteConfig.title,
16 | description = siteConfig.description,
17 | image,
18 | articleDate
19 | } = Astro.props;
20 |
21 | ---
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
19 | {prevUrl && (
20 |
21 | {prevUrl.srLabel && {prevUrl.srLabel} }
22 | {prevUrl.text ? prevUrl.text : "Previous"}
23 |
24 | )}
25 | {nextUrl && (
26 |
27 | {nextUrl.srLabel && {nextUrl.srLabel} }
28 | {nextUrl.text ? nextUrl.text : "Next"}
29 |
30 | )}
31 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "astro-theme-cody",
3 | "type": "module",
4 | "version": "1.3.5",
5 | "scripts": {
6 | "dev": "astro dev",
7 | "start": "astro dev",
8 | "build": "astro check && astro build",
9 | "preview": "astro preview",
10 | "postbuild": "pagefind --site dist && cp -r dist/pagefind public/",
11 | "astro": "astro"
12 | },
13 | "dependencies": {
14 | "@astrojs/check": "^0.5.10",
15 | "@astrojs/mdx": "^2.3.1",
16 | "@astrojs/rss": "^4.0.5",
17 | "@astrojs/sitemap": "^3.1.4",
18 | "@astrojs/tailwind": "^5.1.0",
19 | "astro": "^4.6.3",
20 | "astro-icon": "^0.8.2",
21 | "mdast-util-to-string": "^4.0.0",
22 | "reading-time": "^1.5.0",
23 | "tailwindcss": "^3.3.6",
24 | "typescript": "^5.3.3"
25 | },
26 | "devDependencies": {
27 | "@pagefind/default-ui": "^1.3.0",
28 | "@tailwindcss/typography": "^0.5.10",
29 | "pagefind": "^1.3.0"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/layouts/Base.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import BaseHead from "@/components/BaseHead";
3 | import Header from "@/components/Header";
4 | import Footer from "@/components/Footer";
5 | import { siteConfig } from "../consts.ts";
6 | import { getLangFromUrl } from "@/utils";
7 | import ui from "@/i18n/ui";
8 |
9 | interface Props {
10 | title?: string;
11 | description?: string;
12 | image?: string;
13 | }
14 |
15 | const lang = getLangFromUrl(Astro.url);
16 |
17 | const {
18 | title = siteConfig.title,
19 | description = ui[lang]["site.description"].text,
20 | image,
21 | } = Astro.props;
22 | ---
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/astro.config.mjs:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'astro/config';
2 | import mdx from '@astrojs/mdx';
3 | import sitemap from '@astrojs/sitemap';
4 | import { remarkReadingTime } from './src/utils/remark-reading-time.mjs';
5 |
6 | import tailwind from "@astrojs/tailwind";
7 |
8 | // https://astro.build/config
9 | export default defineConfig({
10 | site: 'https://astro-theme-cody.netlify.app',
11 | integrations: [mdx(), sitemap(), tailwind()],
12 | // NOTE: Make sure this matches your supported languages in the file: src/consts.ts
13 | i18n: {
14 | defaultLocale: "en",
15 | locales: ["en", "es"]
16 | },
17 | markdown: {
18 | remarkPlugins: [remarkReadingTime],
19 | syntaxHighlight: 'shiki',
20 | shikiConfig: {
21 | // https://docs.astro.build/en/guides/markdown-content/#syntax-highlighting
22 | themes: {
23 | light: 'catppuccin-mocha',
24 | dark: 'catppuccin-latte',
25 | },
26 | }
27 | },
28 | });
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/pages/404.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import BaseLayout from '../layouts/Base.astro';
3 | ---
4 |
5 |
6 |
7 |
11 | Looks like you're lost! You sure you know where you're going?
12 |
13 |
14 |
15 |
38 |
--------------------------------------------------------------------------------
/src/components/Footer.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getLangFromUrl, useNavTranslations } from "@/utils";
3 | import { NAV_LINKS, siteConfig } from "../consts.ts";
4 | const year = new Date().getFullYear();
5 | const lang = getLangFromUrl(Astro.url);
6 | const t = useNavTranslations(lang);
7 | ---
8 |
9 |
12 |
13 | Copyright © {year}{" "}
14 | {siteConfig.profile.author}
15 |
16 |
20 | {
21 | NAV_LINKS.map((link) => {
22 | const translation = t(link);
23 | return (
24 |
28 | {translation.text}
29 |
30 | );
31 | })
32 | }
33 |
34 |
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Amy Dang
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 |
--------------------------------------------------------------------------------
/src/content/blog/test-tables.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Test Table"
3 | description: "This is a test post for overflowing tables"
4 | pubDate: 2024-04-21
5 | ---
6 |
7 | This is just a post to test big overflowing tables.
8 | A horizontal scrolling bar should show up at the bottom of the table.
9 |
10 | | A long title | Another long header title | And yet another one :)| 4th time is the charm|
11 | |---|---|---|---|
12 | | Lorem ipsum dolor sit amet, qui minim | labore adipisicing minim sint cillum| sint consectetur cupidatat.| ipsum dolor sit amet, qui minim |
13 | | Lorem ipsum dolor sit amet, qui minim | labore adipisicing minim sint cillum| sint consectetur cupidatat.| ipsum dolor sit amet, qui minim |
14 | | Lorem ipsum dolor sit amet, qui minim | labore adipisicing minim sint cillum| sint consectetur cupidatat.| ipsum dolor sit amet, qui minim |
15 | | Lorem ipsum dolor sit amet, qui minim | labore adipisicing minim sint cillum| sint consectetur cupidatat.| ipsum dolor sit amet, qui minim |
16 | | Lorem ipsum dolor sit amet, qui minim | labore adipisicing minim sint cillum| sint consectetur cupidatat.| ipsum dolor sit amet, qui minim |
17 |
18 |
--------------------------------------------------------------------------------
/src/pages/[lang]/blog/[slug].astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { InferGetStaticPropsType, GetStaticPaths } from "astro";
3 | import BlogPost from "@/layouts/BlogPost";
4 | import {
5 | filterByLanguage,
6 | getAllPosts,
7 | getSlugFromCollectionEntry,
8 | } from "@/utils";
9 | import { getSupportedLanguages, type SupportedLanguage } from "src/utils/i18n";
10 | import type { CollectionEntry } from "astro:content";
11 |
12 | export const getStaticPaths = (async () => {
13 | const blogEntries = await getAllPosts();
14 | return getSupportedLanguages().flatMap((lang) => {
15 | const filteredByLanguage = filterByLanguage(
16 | blogEntries,
17 | lang as SupportedLanguage,
18 | );
19 |
20 | return filteredByLanguage.map((post) => {
21 | const slug = getSlugFromCollectionEntry(
22 | post as CollectionEntry<"blog">,
23 | );
24 | return {
25 | params: { lang, slug },
26 | props: { post },
27 | };
28 | });
29 | });
30 | }) satisfies GetStaticPaths;
31 | export type Props = InferGetStaticPropsType;
32 |
33 | const { post } = Astro.props;
34 | const { Content } = await post.render();
35 | ---
36 |
37 | }>
38 |
39 |
40 |
--------------------------------------------------------------------------------
/src/components/BackToTopBtn.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { Icon } from 'astro-icon';
3 | ---
4 |
5 |
6 |
7 |
8 |
15 |
16 |
36 |
--------------------------------------------------------------------------------
/src/layouts/GenericPost.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Layout from '../layouts/Sidebar.astro';
3 | import { siteConfig } from '../consts';
4 | import SocialMediaLinks from '@/components/SocialMediaLinks';
5 | import TOC from '@/components/TableOfContents';
6 | const { frontmatter: {title, description}, headings } = Astro.props
7 | ---
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
27 |
28 |
--------------------------------------------------------------------------------
/src/pages/[lang]/projects/[slug].astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { InferGetStaticPropsType, GetStaticPaths } from "astro";
3 | import { getCollection, type CollectionEntry } from "astro:content";
4 | import ProjectLayout from "@/layouts/Project";
5 | import {
6 | filterByLanguage,
7 | getSlugFromCollectionEntry,
8 | getSupportedLanguages,
9 | type SupportedLanguage,
10 | } from "@/utils";
11 |
12 | export const getStaticPaths = (async () => {
13 | const allProjects = await getCollection("project", ({ data }) => {
14 | return import.meta.env.PROD ? data.draft !== true : true;
15 | });
16 |
17 | return getSupportedLanguages().flatMap((lang) => {
18 | const projectsFilteredByLang = filterByLanguage(
19 | allProjects,
20 | lang as SupportedLanguage,
21 | );
22 | return projectsFilteredByLang.map((project) => {
23 | const slug = getSlugFromCollectionEntry(project);
24 | return {
25 | params: { lang, slug },
26 | props: { project },
27 | };
28 | });
29 | });
30 | }) satisfies GetStaticPaths;
31 | export type Props = InferGetStaticPropsType;
32 |
33 | const { project } = Astro.props;
34 | const { Content } = await project.render();
35 | ---
36 |
37 | }>
38 |
39 |
40 |
--------------------------------------------------------------------------------
/src/pages/[lang]/series/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getCollection } from "astro:content";
3 | import BaseLayout from "@/layouts/Base";
4 | import {
5 | filterByLanguage,
6 | getSupportedLanguages,
7 | getUniqueWithCountByProperty,
8 | type SupportedLanguage,
9 | } from "@/utils";
10 | import type { GetStaticPaths } from "astro";
11 | import type { CollectionEntry } from "astro:content";
12 |
13 | export const getStaticPaths = (() => {
14 | return getSupportedLanguages().map((lang) => ({ params: { lang } }));
15 | }) satisfies GetStaticPaths;
16 |
17 | const posts = await getCollection("blog", ({ data }) => {
18 | return import.meta.env.PROD ? data.draft !== true : true;
19 | });
20 |
21 | const { lang } = Astro.params;
22 | const filteredByLang = filterByLanguage(posts, lang as SupportedLanguage);
23 | const series = getUniqueWithCountByProperty(
24 | "series",
25 | filteredByLang as Array>,
26 | );
27 | ---
28 |
29 |
30 | Series
31 |
32 | {
33 | series.map(([s, count]) => (
34 |
35 |
36 | #{s}
37 |
38 | ({count})
39 |
40 | ))
41 | }
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/pages/en/about.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Layout from "@/layouts/Base";
3 | ---
4 |
5 |
10 | About
11 |
12 | Hi, I'm a Astro theme. I'm particularly great for starting a blog or a
13 | portfolio.
14 |
30 |
31 |
32 | Here are my built-in features:
33 |
34 | Looks great on any device
35 | Fully Responsive
36 | Comes light and dark mode
37 | Easy to customize and add content
38 | Built with tailwindcss
39 | Pin posts and projects
40 |
41 |
42 | Clone or fork my
43 |
50 | repo
51 |
52 | if you like me!
53 |
54 |
55 |
--------------------------------------------------------------------------------
/src/components/blog/PostPreviewA.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { CollectionEntry } from "astro:content";
3 | import type { HTMLTag, Polymorphic } from "astro/types";
4 | import FormattedDate from "@/components/FormattedDate";
5 | import { Icon } from "astro-icon";
6 | import { getLangFromUrl, getSlugFromCollectionEntry } from "@/utils";
7 |
8 | type Props = Polymorphic<{ as: Tag }> & {
9 | post: CollectionEntry<"blog" | "project">;
10 | withDesc?: boolean;
11 | };
12 |
13 | const { post, withDesc = false } = Astro.props;
14 | const postDate = post.data.updatedDate ?? post.data.pubDate;
15 | const lang = getLangFromUrl(Astro.url);
16 | const slug = getSlugFromCollectionEntry(post);
17 | const pinned: boolean = !!post.data.order;
18 | const urlBasePath =
19 | post.collection === "project" ? "projects" : post.collection;
20 | ---
21 |
22 |
23 |
24 |
25 | {post.data.title}
26 |
27 | {
28 | pinned && (
29 |
35 | )
36 | }
37 |
38 |
39 |
40 |
41 | {withDesc && {post.data.description}
}
42 |
43 |
49 |
--------------------------------------------------------------------------------
/src/pages/es/about.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Layout from "@/layouts/Base";
3 | ---
4 |
5 |
10 | Acerca de
11 |
12 | Hola, soy un tema Astro. Soy particularmente bueno para comenzar un blog
13 | o un portafolio.
14 |
15 |
31 |
32 |
33 | A continuación, se muestran mis características integradas:
34 |
35 |
36 | Se ve genial en cualquier dispositivo
37 | Totalmente responsivo
38 | Viene con en modo claro y oscuro
39 | Fácil de personalizar y agregar contenido
40 | Construido con tailwindcss
41 | Fija publicaciones y proyectos
42 |
43 |
44 | ¡Clona o bifurca mi
45 |
52 | repositorio
53 |
54 | si te gusto!
55 |
56 |
57 |
--------------------------------------------------------------------------------
/src/components/blog/PostPreviewB.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { CollectionEntry } from "astro:content";
3 | import type { HTMLTag, Polymorphic } from "astro/types";
4 | import FormattedDate from "@/components/FormattedDate";
5 | import { Icon } from "astro-icon";
6 | import { getLangFromUrl, getSlugFromCollectionEntry } from "@/utils";
7 |
8 | type Props = Polymorphic<{ as: Tag }> & {
9 | post: CollectionEntry<"blog">;
10 | withDesc?: boolean;
11 | };
12 |
13 | const { post, withDesc = false } = Astro.props;
14 | const lang = getLangFromUrl(Astro.url);
15 | const slug = getSlugFromCollectionEntry(post);
16 | const urlBasePath =
17 | post.collection === "project" ? "projects" : post.collection;
18 | const postDate = post.data.updatedDate ?? post.data.pubDate;
19 | const pinned: boolean = !!post.data.order;
20 | ---
21 |
22 |
23 |
48 | {withDesc && {post.data.description}
}
49 |
50 |
--------------------------------------------------------------------------------
/src/content/blog/using-mdx.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Using MDX'
3 | description: 'Lorem ipsum dolor sit amet'
4 | pubDate: 'Jul 02 2022'
5 | tags: ["guide"]
6 | ---
7 |
8 | This theme comes with the [@astrojs/mdx](https://docs.astro.build/en/guides/integrations-guide/mdx/) integration installed and configured in your `astro.config.mjs` config file. If you prefer not to use MDX, you can disable support by removing the integration from your config file.
9 |
10 | ## Why MDX?
11 |
12 | MDX is a special flavor of Markdown that supports embedded JavaScript & JSX syntax. This unlocks the ability to [mix JavaScript and UI Components into your Markdown content](https://docs.astro.build/en/guides/markdown-content/#mdx-features) for things like interactive charts or alerts.
13 |
14 | If you have existing content authored in MDX, this integration will hopefully make migrating to Astro a breeze.
15 |
16 | ## Example
17 |
18 | Here is how you import and use a UI component inside of MDX.
19 | When you open this page in the browser, you should see the clickable button below.
20 |
21 | import HeaderLink from '../../components/HeaderLink.astro';
22 |
23 |
24 | Embedded component in MDX
25 |
26 |
27 | ## More Links
28 |
29 | - [MDX Syntax Documentation](https://mdxjs.com/docs/what-is-mdx)
30 | - [Astro Usage Documentation](https://docs.astro.build/en/guides/markdown-content/#markdown-and-mdx-pages)
31 | - **Note:** [Client Directives](https://docs.astro.build/en/reference/directives-reference/#client-directives) are still required to create interactive components. Otherwise, all components in your MDX will render as static HTML (no JavaScript) by default.
32 |
--------------------------------------------------------------------------------
/src/utils/i18n.ts:
--------------------------------------------------------------------------------
1 | import { DEFAULT_LANG, SUPPORTED_LANGUAGES } from "src/consts";
2 | import nav, { type NavEntry } from '@/i18n/nav';
3 | import ui, { type UIEntry } from "@/i18n/ui";
4 |
5 | export type SupportedLanguage = keyof typeof SUPPORTED_LANGUAGES;
6 |
7 | export function getSupportedLanguages(): string[] {
8 | return Object.keys(SUPPORTED_LANGUAGES);
9 | }
10 |
11 | export function isValidLanguageCode(lang: string): boolean {
12 | return Object.hasOwn(SUPPORTED_LANGUAGES, lang);
13 | }
14 |
15 | export function getLangFromUrl(url: URL) {
16 | const [, lang,] = url.pathname.split('/');
17 | if (lang in SUPPORTED_LANGUAGES) {
18 | return lang as SupportedLanguage;
19 | }
20 | return DEFAULT_LANG;
21 | }
22 |
23 | export function getLangFromSlug(slug: string) {
24 | const [lang,] = slug.split('/');
25 | if (lang in SUPPORTED_LANGUAGES) {
26 | return lang as SupportedLanguage;
27 | }
28 | return DEFAULT_LANG;
29 | }
30 |
31 | export function getLocalizedUrl(url: URL, locale: SupportedLanguage): string {
32 | const [, , ...slug] = url.pathname.split('/');
33 | if (isValidLanguageCode(locale)) {
34 | return `/${locale}/${slug.join("/")}`
35 | }
36 | return `/${DEFAULT_LANG}/${slug.join("/")}`
37 | }
38 |
39 | export function useNavTranslations(lang: keyof typeof nav) {
40 | return function t(key: keyof typeof nav[SupportedLanguage]): NavEntry {
41 | return nav[lang][key] || nav[DEFAULT_LANG][key];
42 | }
43 | }
44 |
45 | export function useUITranslations(lang: keyof typeof nav) {
46 | return function t(key: keyof typeof ui[SupportedLanguage]): UIEntry {
47 | return ui[lang][key] || ui[DEFAULT_LANG][key];
48 | }
49 | }
--------------------------------------------------------------------------------
/src/pages/[lang]/projects/pages/[page].astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { Page, GetStaticPaths } from "astro";
3 | import type { CollectionEntry } from "astro:content";
4 | import BaseLayout from "@/layouts/Base";
5 | import PostPreview from "@/components/blog/PostPreviewA";
6 | import { getCollection } from "astro:content";
7 | import { siteConfig } from "src/consts";
8 | import {
9 | getSupportedLanguages,
10 | filterByLanguage,
11 | type SupportedLanguage,
12 | } from "@/utils";
13 |
14 | interface Props {
15 | page: Page>;
16 | }
17 |
18 | export const getStaticPaths = (async ({ paginate }) => {
19 | const allProjects = await getCollection("project", ({ data }) => {
20 | return import.meta.env.PROD ? data.draft !== true : true;
21 | });
22 |
23 | const sortedByDateProjects = allProjects.sort((a, b) => {
24 | const aDate = new Date(a.data.pubDate).valueOf();
25 | const bDate = new Date(b.data.pubDate).valueOf();
26 | return bDate - aDate;
27 | });
28 |
29 | // move all pinned projects to the top
30 | const sortedByPinned = sortedByDateProjects.sort((a, b) => {
31 | const aOrder = a.data.order ?? 100;
32 | const bOrder = b.data.order ?? 100;
33 | return aOrder - bOrder;
34 | });
35 |
36 | return getSupportedLanguages().flatMap((lang) => {
37 | const projects = filterByLanguage(
38 | sortedByPinned,
39 | lang as SupportedLanguage,
40 | );
41 |
42 | return paginate(projects, {
43 | params: { lang },
44 | pageSize: siteConfig.settings.paginationSize,
45 | });
46 | });
47 | }) satisfies GetStaticPaths;
48 |
49 | const { page } = Astro.props;
50 | ---
51 |
52 |
53 |
54 | Projects
55 |
56 | {page.data.map((p) => )}
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/src/styles/search.css:
--------------------------------------------------------------------------------
1 | /* @import "@pagefind/default-ui/css/ui.css"; */
2 |
3 | @tailwind base;
4 | @tailwind components;
5 | @tailwind utilities;
6 |
7 | #cody-search,
8 | .pagefind-ui__form,
9 | .pagfind-ui {
10 | width: 100%;
11 | }
12 |
13 | .pagefind-ui__form {
14 | display: flex;
15 | flex-direction: row;
16 | justify-content: space-between;
17 | gap: 1rem;
18 | flex-wrap: wrap;
19 | }
20 |
21 | input.pagefind-ui__search-input {
22 | width: 94%;
23 | @apply p-2 border-b border-accent bg-surface;
24 | }
25 |
26 | .pagefind-ui__search-input:focus-visible {
27 | @apply !outline-none border border-accent;
28 | }
29 |
30 | .pagefind-ui__search-input {
31 | flex-grow: 1 0;
32 | }
33 |
34 | .pagefind-ui__search-clear {
35 | flex-grow: 1 0;
36 | }
37 |
38 | .pagefind-ui__search-clear:hover {
39 | @apply text-accent;
40 | }
41 |
42 | .pagefind-ui__drawer,
43 | .pagefind-ui__results-area {
44 | display: flex;
45 | flex-direction: column;
46 | flex-basis: 100%;
47 | }
48 |
49 | .pagefind-ui__message {
50 | @apply font-bold text-accent;
51 | }
52 |
53 | .pagefind-ui__results>li {
54 | @apply !my-2;
55 | }
56 |
57 | .pagefind-ui__result-title:hover {
58 | @apply decoration-accent text-accent;
59 | }
60 |
61 | .pagefind-ui__result-link:focus-visible {
62 | @apply outline outline-accent outline-1;
63 | }
64 |
65 | .pagefind-ui__result-title {
66 | @apply underline underline-offset-2 text-lg;
67 | }
68 |
69 | .pagefind-ui__result-excerpt>mark {
70 | @apply text-accent bg-surface;
71 | }
72 |
73 | .pagefind-ui__result-nested {
74 | @apply ml-6;
75 | }
76 |
77 | .pagefind-ui__result-nested>.pagefind-ui__result-title::before {
78 | content: "\21AA";
79 | @apply pr-2 no-underline;
80 | }
81 |
82 | .pagefind-ui__button {
83 | @apply underline decoration-accent underline-offset-2;
84 | }
85 |
86 | .pagefind-ui__button:hover {
87 | @apply text-accent;
88 | }
--------------------------------------------------------------------------------
/src/components/SelectLanguage.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import {
3 | getLangFromUrl,
4 | getLocalizedUrl,
5 | getSupportedLanguages,
6 | type SupportedLanguage,
7 | } from "src/utils/i18n";
8 |
9 | const { class: className } = Astro.props;
10 |
11 | let currentLanguage = getLangFromUrl(Astro.url);
12 | ---
13 |
14 |
15 |
16 | Select Language
17 |
23 | {
24 | getSupportedLanguages().map((lang: string) => {
25 | return (
26 |
35 | );
36 | })
37 | }
38 |
39 |
40 |
41 |
42 |
59 |
60 |
65 |
--------------------------------------------------------------------------------
/src/pages/[lang]/tags/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getCollection } from "astro:content";
3 | import BaseLayout from "@/layouts/Base";
4 | import {
5 | filterByLanguage,
6 | getSupportedLanguages,
7 | getUniqueWithCountByProperty,
8 | type SupportedLanguage,
9 | } from "@/utils";
10 | import type { GetStaticPaths } from "astro";
11 | import type { CollectionEntry } from "astro:content";
12 |
13 | export const getStaticPaths = (() => {
14 | return getSupportedLanguages().map((lang) => ({ params: { lang } }));
15 | }) satisfies GetStaticPaths;
16 |
17 | const posts = await getCollection("blog", ({ data }) => {
18 | return import.meta.env.PROD ? data.draft !== true : true;
19 | });
20 |
21 | const { lang } = Astro.params;
22 | const filteredByLang = filterByLanguage(posts, lang as SupportedLanguage);
23 | const tags = getUniqueWithCountByProperty(
24 | "tags",
25 | filteredByLang as Array>,
26 | );
27 | ---
28 |
29 |
30 |
31 |
42 |
43 |
46 |
49 |
50 |
51 | Tags
52 |
53 |
54 | {
55 | tags.map(([tag, count]) => (
56 |
57 |
58 | #{tag}
59 |
60 | ({count})
61 |
62 | ))
63 | }
64 |
65 |
66 |
--------------------------------------------------------------------------------
/src/consts.ts:
--------------------------------------------------------------------------------
1 | // This is your config file, place any global data here.
2 | // You can import this data from anywhere in your site by using the `import` keyword.
3 |
4 | import type nav from "./i18n/nav";
5 | import ui from "./i18n/ui";
6 | import type { SupportedLanguage } from "./utils/i18n";
7 |
8 | type Config = {
9 | title: string;
10 | description: string;
11 | lang: string;
12 | profile: {
13 | author: string;
14 | description?: string;
15 | },
16 | settings: {
17 | paginationSize: number,
18 | },
19 | }
20 |
21 | type SocialLink = {
22 | icon: string;
23 | friendlyName: string; // for accessibility
24 | link: string;
25 | }
26 |
27 | export const SUPPORTED_LANGUAGES = {
28 | 'en': 'en',
29 | 'es': 'es'
30 | };
31 |
32 | export const DEFAULT_LANG = SUPPORTED_LANGUAGES.en as SupportedLanguage;
33 |
34 | export const siteConfig: Config = {
35 | title: ui[DEFAULT_LANG]["site.title"].text,
36 | description: ui[DEFAULT_LANG]["site.description"].text,
37 | lang: DEFAULT_LANG,
38 | profile: {
39 | author: "Amy Dang",
40 | description: ui[DEFAULT_LANG]["profile.description"].text
41 | },
42 | settings: {
43 | paginationSize: 10
44 | }
45 | }
46 |
47 | /**
48 | These are you social media links.
49 | It uses https://github.com/natemoo-re/astro-icon#readme
50 | You can find icons @ https://icones.js.org/
51 | */
52 | export const SOCIAL_LINKS: Array = [
53 | {
54 | icon: "mdi:github",
55 | friendlyName: "Github",
56 | link: "https://github.com/kirontoo/astro-theme-cody",
57 | },
58 | {
59 | icon: "mdi:linkedin",
60 | friendlyName: "LinkedIn",
61 | link: "#",
62 | },
63 | {
64 | icon: "mdi:email",
65 | friendlyName: "email",
66 | link: "mailto:ndangamy@gmail.com",
67 | },
68 | {
69 | icon: "mdi:rss",
70 | friendlyName: "rss",
71 | link: "/rss.xml"
72 | }
73 | ];
74 |
75 | // NOTE: match these entries with keys in `src/i18n/nav.ts`
76 | export const NAV_LINKS: Array = [
77 | "home", "about", "blog", "projects", "archive"
78 | ];
79 |
--------------------------------------------------------------------------------
/src/content/config.ts:
--------------------------------------------------------------------------------
1 | import { defineCollection, z } from 'astro:content';
2 |
3 | function removeDupsAndLowercase(list: string[]) {
4 | if (!list.length) return list;
5 | const lowercaseItems = list.map((str) => str.toLowerCase());
6 | const uniqueItems = new Set(lowercaseItems);
7 | return Array.from(uniqueItems);
8 |
9 | }
10 |
11 | const blog = defineCollection({
12 | type: 'content',
13 | // Type-check frontmatter using a schema
14 | schema: () => z.object({
15 | title: z.string().max(150),
16 | description: z.string().max(250),
17 | // Transform string to Date object
18 | pubDate: z
19 | .string()
20 | .or(z.date())
21 | .transform(val => new Date(val)),
22 | updatedDate: z
23 | .string()
24 | .or(z.date())
25 | .transform(val => val ? new Date(val) : undefined)
26 | .optional(),
27 | heroImage: z.object({
28 | src: z.string(),
29 | alt: z.string().optional(),
30 | }).optional(),
31 | ogImage: z.string().optional(),
32 | tags: z
33 | .array(z.string())
34 | .default([])
35 | .transform(removeDupsAndLowercase)
36 | .optional(),
37 | series: z.string().optional(),
38 | draft: z.boolean().optional().default(false),
39 | // for pinning posts
40 | order: z.number().min(1).max(5).optional(),
41 | // hide a post from pagination
42 | hide: z.boolean().optional().default(false)
43 | }),
44 | });
45 |
46 | const project = defineCollection({
47 | type: 'content',
48 | schema: () => z.object({
49 | title: z.string(),
50 | description: z.string(),
51 | pubDate: z
52 | .string()
53 | .or(z.date())
54 | .transform(val => new Date(val)),
55 | heroImage: z.object({
56 | url: z.string(),
57 | alt: z.string().optional()
58 | }).optional(),
59 | ogImage: z.string().optional(),
60 | stack: z.array(z.string()).default([]).transform(removeDupsAndLowercase),
61 | platform: z.string().optional(),
62 | website: z.string().optional(),
63 | github: z.string().optional(),
64 | draft: z.boolean().optional().default(false),
65 | // for pinning projects
66 | order: z.number().min(1).max(5).optional()
67 | })
68 | });
69 |
70 | export const collections = { blog, project };
71 |
--------------------------------------------------------------------------------
/src/i18n/ui.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This configures the translations for all ui text in your website.
3 | *
4 | * All languages will follow this ordering/structure and will fallback to the
5 | * default language for any entries that haven't been translated
6 | */
7 | import type { SupportedLanguage } from "src/utils/i18n";
8 |
9 | export default {
10 | "en": {
11 | "site.title": {
12 | text: "Astro Theme Cody"
13 | },
14 | "site.description": {
15 | text: "A minimalist blog theme built with Astro. A quick and easy starter build for anyone who wants to start their own blog."
16 | },
17 | "profile.description": {
18 | text: "your bio description"
19 | },
20 | "blog.lastUpdated": {
21 | text: "Last updated:"
22 | },
23 | "sidebar.tableOfContents": {
24 | text: "Table of Contents"
25 | },
26 | "project.platform": {
27 | text: "PLATFORM"
28 | },
29 | "project.stack": {
30 | text: "STACK"
31 | },
32 | "project.website": {
33 | text: "WEBSITE"
34 | }
35 | },
36 | "es": {
37 | "site.title": {
38 | text: "Astro Theme Cody"
39 | },
40 | "site.description": {
41 | text: "Un tema de blog minimalista creado con Astro. Un tema de inicio rápido y sencillo para cualquiera que quiera crear su propio blog."
42 | },
43 | "profile.description": {
44 | text: "tu descripción biográfica"
45 | },
46 | "blog.lastUpdated": {
47 | text: "Última actualización:"
48 | },
49 | "sidebar.tableOfContents": {
50 | text: "Tabla de contenidos"
51 | },
52 | "project.platform": {
53 | text: "PLATAFORMA"
54 | },
55 | "project.stack": {
56 | text: "PILA"
57 | },
58 | "project.website": {
59 | text: "WEBSITE"
60 | }
61 | }
62 | } as const satisfies TranslationUIEntries;
63 |
64 | type TranslationUIEntries = Record>;
65 |
66 | export type UIEntry = { text: string };
--------------------------------------------------------------------------------
/src/components/CopyBtn.astro:
--------------------------------------------------------------------------------
1 |
51 |
--------------------------------------------------------------------------------
/src/pages/[lang]/tags/[tag]/[...page].astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { Page, GetStaticPaths } from "astro";
3 | import type { CollectionEntry } from "astro:content";
4 | import BaseLayout from "@/layouts/Base";
5 | import {
6 | getAllPosts,
7 | getSupportedLanguages,
8 | filterByLanguage,
9 | getUniqueTags,
10 | sortMDByDate,
11 | getPostsByTag,
12 | type SupportedLanguage,
13 | } from "@/utils";
14 | import PostPreview from "@/components/blog/PostPreviewA";
15 | // Alternative post item
16 | // import PostPreview from '@/components/blog/PostPreviewB';
17 |
18 | import Pagination from "@/components/Pagination";
19 | import { siteConfig } from "src/consts";
20 |
21 | export const getStaticPaths: GetStaticPaths = async ({ paginate }) => {
22 | const allPosts = await getAllPosts();
23 | const allPostsByDate = sortMDByDate(allPosts);
24 | const uniqueTags = getUniqueTags(allPostsByDate);
25 |
26 | return getSupportedLanguages().flatMap((lang) => {
27 | return uniqueTags.flatMap((tag) => {
28 | const postsFilteredByTag = getPostsByTag(tag, allPostsByDate);
29 | const postsFilteredByLang = filterByLanguage(
30 | postsFilteredByTag,
31 | lang as SupportedLanguage,
32 | );
33 | return paginate(postsFilteredByLang, {
34 | params: { lang, tag },
35 | pageSize: siteConfig.settings.paginationSize,
36 | });
37 | });
38 | });
39 | };
40 |
41 | interface Props {
42 | page: Page>;
43 | }
44 |
45 | const { page } = Astro.props;
46 | const { tag, lang } = Astro.params;
47 |
48 | const meta = {
49 | title: `Tag: ${tag}`,
50 | description: `View all posts with the tag - ${tag}`,
51 | };
52 |
53 | const paginationProps = {
54 | ...(page.url.prev && {
55 | prevUrl: {
56 | url: page.url.prev,
57 | text: `← Previous Tags`,
58 | },
59 | }),
60 | ...(page.url.next && {
61 | nextUrl: {
62 | url: page.url.next,
63 | text: `Next Tags →`,
64 | },
65 | }),
66 | };
67 | ---
68 |
69 |
70 |
71 | Tags
74 | →
75 | #{tag}
76 |
77 |
78 | {page.data.map((post) => )}
79 |
80 |
81 |
82 |
--------------------------------------------------------------------------------
/src/i18n/nav.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This configures the navigation bar and footer. Each entry is a nav link with
3 | * the correct translation and url path.
4 | *
5 | * All languages will follow this ordering/structure and will fallback to the
6 | * default language for any entries that haven't been translated
7 | */
8 |
9 | import type { SupportedLanguage } from "src/utils/i18n";
10 |
11 | export default {
12 | "en": {
13 | "home": {
14 | text: "Home",
15 | slug: ""
16 | },
17 | "about": {
18 | text: "About",
19 | slug: "about"
20 | },
21 | "blog": {
22 | text: "Blog",
23 | slug: "blog",
24 | route: "/blog/pages/1"
25 | },
26 | "projects": {
27 | text: "Projects",
28 | slug: "projects",
29 | route: "/projects/pages/1"
30 | },
31 | "archive": {
32 | text: "Archive",
33 | slug: "archive"
34 | },
35 | "tags": {
36 | text: "Tags",
37 | slug: "tags"
38 | },
39 | "series": {
40 | text: "Series",
41 | slug: "series"
42 | }
43 | },
44 | "es": {
45 | "home": {
46 | text: "Página Principal",
47 | slug: ""
48 | },
49 | "about": {
50 | text: "Acerca De",
51 | slug: "about"
52 | },
53 | "blog": {
54 | text: "Blog",
55 | slug: "blog",
56 | route: "/blog/pages/1"
57 | },
58 | "projects": {
59 | text: "Proyectos",
60 | slug: "projects",
61 | route: "/projects/pages/1"
62 | },
63 | "archive": {
64 | text: "Archivo",
65 | slug: "archive"
66 | },
67 | "tags": {
68 | text: "Etiquetas",
69 | slug: "tags"
70 | },
71 | "series": {
72 | text: "Serie",
73 | slug: "series"
74 | }
75 | }
76 | } as const satisfies TranslationNavEntries;
77 |
78 | type TranslationNavEntries = Record>
79 |
80 | export type NavEntry = {
81 | /*
82 | Provided translation
83 | */
84 | text: string,
85 |
86 | /*
87 | Content collection slug or url path for this page without the language code
88 | */
89 | slug: string,
90 |
91 | route?: string
92 | };
--------------------------------------------------------------------------------
/src/pages/[lang]/series/[series]/[...page].astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { Page, GetStaticPaths } from "astro";
3 | import type { CollectionEntry } from "astro:content";
4 | import BaseLayout from "@/layouts/Base";
5 | import {
6 | getAllPosts,
7 | getUniqueByProperty,
8 | sortMDByDate,
9 | getPostsBySeries,
10 | getSupportedLanguages,
11 | filterByLanguage,
12 | type SupportedLanguage,
13 | } from "@/utils";
14 | import PostPreview from "@/components/blog/PostPreviewA";
15 | // Alternative post item
16 | // import PostPreview from '@/components/blog/PostPreviewB';
17 |
18 | import Pagination from "@/components/Pagination";
19 | import { siteConfig } from "src/consts";
20 |
21 | export const getStaticPaths: GetStaticPaths = async ({ paginate }) => {
22 | const allPosts = await getAllPosts();
23 | const allPostsByDate = sortMDByDate(allPosts, "ascending");
24 | const uniqueSeries = getUniqueByProperty("series", allPostsByDate);
25 |
26 | return getSupportedLanguages().flatMap((lang) => {
27 | return uniqueSeries.flatMap((series) => {
28 | const postsFilteredBySeries = getPostsBySeries(
29 | series,
30 | allPostsByDate,
31 | );
32 | const postsFilteredByLang = filterByLanguage(
33 | postsFilteredBySeries,
34 | lang as SupportedLanguage,
35 | );
36 | return paginate(postsFilteredByLang, {
37 | params: { lang, series },
38 | pageSize: siteConfig.settings.paginationSize,
39 | });
40 | });
41 | });
42 | };
43 |
44 | interface Props {
45 | page: Page>;
46 | }
47 |
48 | const { page } = Astro.props;
49 | const { series } = Astro.params;
50 |
51 | const meta = {
52 | title: `Series: ${series}`,
53 | description: `View all posts with the series - ${series}`,
54 | };
55 |
56 | const paginationProps = {
57 | ...(page.url.prev && {
58 | prevUrl: {
59 | url: page.url.prev,
60 | text: `← Previous in ${series}`,
61 | },
62 | }),
63 | ...(page.url.next && {
64 | nextUrl: {
65 | url: page.url.next,
66 | text: `Next in ${series} →`,
67 | },
68 | }),
69 | };
70 | ---
71 |
72 |
73 |
74 | Series
75 | →
76 | #{series}
77 |
78 |
79 | {page.data.map((post) => )}
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/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";
5 | import { siteConfig } from "../consts.ts";
6 |
7 | interface Props {
8 | title: string;
9 | description: string;
10 | image?: string;
11 | articleDate?: string;
12 | }
13 |
14 | const canonicalURL = new URL(Astro.url.pathname, Astro.site);
15 |
16 | const {
17 | title,
18 | description,
19 | articleDate,
20 | image = "/blog-placeholder-1.jpg",
21 | } = Astro.props;
22 | const titleSeparator = "|";
23 | const siteTitle = `${title} ${titleSeparator} ${siteConfig.title}`;
24 | ---
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
39 |
40 |
41 |
42 |
43 |
44 | {siteTitle}
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | {
56 | articleDate && (
57 | <>
58 |
59 |
60 | >
61 | )
62 | }
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
74 |
--------------------------------------------------------------------------------
/src/utils/post.ts:
--------------------------------------------------------------------------------
1 | import { getCollection, type CollectionEntry } from "astro:content";
2 | import { getLangFromSlug, type SupportedLanguage } from "./i18n";
3 |
4 | export async function getAllPosts(filterHidden: boolean = false) {
5 | return await getCollection("blog", ({ data }) => {
6 | if (import.meta.env.PROD) {
7 | if (filterHidden) {
8 | return !data.hide;
9 | }
10 |
11 | // on production: exclude draft posts by default
12 | return !data.draft;
13 | }
14 |
15 | return filterHidden ? !data.hide : true;
16 | });
17 | }
18 |
19 | // ascending = oldest to newest date
20 | // descending = newest to oldest date
21 | export function sortMDByDate(
22 | posts: Array>,
23 | order: "ascending" | "descending" = "descending"
24 | ) {
25 | // -1 = ascending
26 | // 1 = descending
27 | const direction = order === "descending" ? 1 : -1;
28 |
29 | return posts.sort((a, b) => {
30 | const aDate = new Date(a.data.updatedDate ?? a.data.pubDate).valueOf();
31 | const bDate = new Date(b.data.updatedDate ?? b.data.pubDate).valueOf();
32 | return (bDate - aDate) * direction;
33 | });
34 | }
35 |
36 |
37 | export function sortMDByPinned(posts: Array>) {
38 | return posts.sort((a, b) => {
39 | const aOrder = a.data.order ?? 100;
40 | const bOrder = b.data.order ?? 100;
41 | return aOrder - bOrder;
42 | });
43 | }
44 |
45 | export function filterByLanguage(posts: Array>, lang: SupportedLanguage): Array> {
46 | return posts.filter((post) => {
47 | const translationLang = getLangFromSlug(post.slug);
48 | return lang === translationLang;
49 | });
50 | }
51 |
52 | export function getPostsByTag(
53 | tag: string,
54 | posts: Array>
55 | ) {
56 | return posts.filter(post => {
57 | if (post.data.tags) {
58 | return post.data.tags.includes(tag);
59 | }
60 | return false;
61 | });
62 | }
63 |
64 | export function getPostsBySeries(
65 | series: string,
66 | posts: Array>
67 | ) {
68 | return posts.filter(post => {
69 | if (post.data.series) {
70 | return post.data.series.includes(series);
71 | }
72 | return false;
73 | });
74 | }
75 |
76 | // Possible slugs: "[lang]/[slug]" or "[slug]"
77 | export function getSlugFromCollectionEntry(entry: CollectionEntry<"blog" | "project">) {
78 | const [, ...slugs] = entry.slug.split("/");
79 | // if collection entry is a translation, grab the slug only
80 | return slugs.length ? slugs.join("/") : entry.slug;
81 | }
--------------------------------------------------------------------------------
/src/components/ThemeToggle.astro:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
32 |
33 |
62 |
--------------------------------------------------------------------------------
/src/components/TableOfContents.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { MarkdownHeading } from "astro";
3 | import TableOfContentsHeading from "@/components/TableOfContentsHeading";
4 | import { useUITranslations, type SupportedLanguage } from "@/utils";
5 | const { headings } = Astro.props;
6 | const toc = buildToc(headings);
7 |
8 | // https://github.com/chrismwilliams/astro-theme-cactus/blob/a85e0e559d3f92b32e73990486c0574b2b733227/src/utils/generateToc.ts
9 | export interface TocItem extends MarkdownHeading {
10 | subheadings: Array;
11 | }
12 |
13 | function diveChildren(item: TocItem, depth: number): Array {
14 | if (depth === 1 || !item.subheadings.length) {
15 | return item.subheadings;
16 | } else {
17 | // e.g., 2
18 | return diveChildren(
19 | item.subheadings[item.subheadings.length - 1] as TocItem,
20 | depth - 1,
21 | );
22 | }
23 | }
24 |
25 | function buildToc(headings: ReadonlyArray) {
26 | // this ignores/filters out h1 element(s)
27 | const bodyHeadings = headings.filter(({ depth }) => depth > 1);
28 | const toc: Array = [];
29 |
30 | bodyHeadings.forEach((h) => {
31 | const heading: TocItem = { ...h, subheadings: [] };
32 |
33 | // add h2 elements into the top level
34 | if (heading.depth === 2) {
35 | toc.push(heading);
36 | } else {
37 | const lastItemInToc = toc[toc.length - 1]!;
38 | if (heading.depth < lastItemInToc.depth) {
39 | throw new Error(`Orphan heading found: ${heading.text}.`);
40 | }
41 |
42 | // higher depth
43 | // push into children, or children's children
44 | const gap = heading.depth - lastItemInToc.depth;
45 | const target = diveChildren(lastItemInToc, gap);
46 | target.push(heading);
47 | }
48 | });
49 | return toc;
50 | }
51 |
52 | const { lang } = Astro.params;
53 | const translate = useUITranslations(lang as SupportedLanguage);
54 | ---
55 |
56 |
57 |
58 |
59 | {translate("sidebar.tableOfContents").text}
60 |
61 |
62 | {toc.map((heading) => )}
63 |
64 |
65 |
66 |
85 |
--------------------------------------------------------------------------------
/src/pages/[lang]/archive/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { GetStaticPaths } from "astro";
3 | import {
4 | filterByLanguage,
5 | getAllPosts,
6 | getSlugFromCollectionEntry,
7 | sortMDByDate,
8 | type SupportedLanguage,
9 | } from "@/utils";
10 | import BaseLayout from "@/layouts/Base";
11 | import FormattedDate from "@/components/FormattedDate";
12 | import { siteConfig } from "src/consts";
13 | import type { CollectionEntry } from "astro:content";
14 | import { getSupportedLanguages } from "@/utils";
15 |
16 | const allPosts = await getAllPosts();
17 | const allPostsByDate = sortMDByDate(allPosts, "ascending");
18 |
19 | export const getStaticPaths = (() => {
20 | return getSupportedLanguages().map((lang) => ({ params: { lang } }));
21 | }) satisfies GetStaticPaths;
22 |
23 | const { lang } = Astro.params;
24 | const postsFilteredByLanguage = filterByLanguage(
25 | allPostsByDate,
26 | lang as SupportedLanguage,
27 | );
28 |
29 | const getGroupsByYear = (
30 | posts: Array>,
31 | ): Record>> => {
32 | const initialValue: Record>> = {};
33 | return posts.reduce((groups, p) => {
34 | const date = new Date(p.data.pubDate);
35 | const year: string = date.getFullYear().toString();
36 | if (!groups[year]) {
37 | groups[year] = [];
38 | }
39 | groups[year].push(p);
40 | return groups;
41 | }, initialValue);
42 | };
43 |
44 | const groupArray = (posts: Array>) => {
45 | const groups = getGroupsByYear(posts);
46 | const keys: Array = Object.keys(groups);
47 | return keys.map((year) => {
48 | return {
49 | year,
50 | posts: groups[year],
51 | };
52 | });
53 | };
54 |
55 | const archivedPosts = groupArray(
56 | postsFilteredByLanguage as Array>,
57 | ).reverse();
58 | const dateFormat = {
59 | locale: "en-US",
60 | options: {
61 | month: "short",
62 | day: "2-digit",
63 | } as Intl.DateTimeFormatOptions,
64 | };
65 | ---
66 |
67 |
68 |
69 | {
70 | archivedPosts.map((group) => (
71 |
93 | ))
94 | }
95 |
96 |
97 |
--------------------------------------------------------------------------------
/src/pages/en/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import BaseLayout from "@/layouts/Base";
3 | import PostPreview from "@/components/blog/PostPreviewB";
4 | import SocialMediaLinks from "@/components/SocialMediaLinks";
5 | import {
6 | filterByLanguage,
7 | getAllPosts,
8 | sortMDByDate,
9 | sortMDByPinned,
10 | } from "@/utils";
11 | import type { CollectionEntry } from "astro:content";
12 |
13 | const MAX_POSTS = 10;
14 | const allPosts = await getAllPosts();
15 | const allPostsByDate = sortMDByDate(allPosts);
16 | const allPostsByPinned = sortMDByPinned(allPostsByDate);
17 | const filteredPostsByLanguage = filterByLanguage(allPostsByPinned, "en").slice(0, MAX_POSTS);
18 |
19 | const tech: Array<{ title: string; desc: string; href: string }> = [
20 | {
21 | title: "Astro",
22 | desc: "Build fast websites, faster.",
23 | href: "https://astro.build",
24 | },
25 | {
26 | title: "Astro Assets",
27 | desc: "Built-in optimized asset support.",
28 | href: "https://docs.astro.build/en/guides/assets/",
29 | },
30 | {
31 | title: "Tailwind CSS",
32 | desc: "Rapidly build modern websites without ever leaving your HTML.",
33 | href: "https://tailwindcss.com",
34 | },
35 | {
36 | title: "Markdown",
37 | desc: "Simple and easy-to-use markup language.",
38 | href: "https://www.markdownguide.org/",
39 | },
40 | {
41 | title: "MDX",
42 | desc: "Markdown for the component era.",
43 | href: "https://mdxjs.com/",
44 | },
45 | {
46 | title: "Astro Icon",
47 | desc: "An easy to use Icon component for Astro.",
48 | href: "https://github.com/natemoo-re/astro-icon#readme",
49 | },
50 | ];
51 | ---
52 |
53 |
54 |
55 | Hello World
56 |
57 | Hey guys! This a theme for Astro. You can use this to as personal
58 | blog or portfolio. If you want to know more about how you can
59 | customise and make it your own, click on the github icon link below,
60 | and it will take you to the repo.
61 |
62 |
63 | You can find me on:
64 |
65 |
66 |
67 |
68 |
69 | Posts
70 |
71 | {
72 | filteredPostsByLanguage.map((p) => (
73 | } />
74 | ))
75 | }
76 |
77 |
78 |
79 |
80 | Technologies
81 |
82 | {
83 | tech.map(({ href, title, desc }) => (
84 |
85 |
91 | {title}
92 |
93 | : {desc}
94 |
95 | ))
96 | }
97 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/src/pages/es/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import BaseLayout from "@/layouts/Base";
3 | import PostPreview from "@/components/blog/PostPreviewB";
4 | import SocialMediaLinks from "@/components/SocialMediaLinks";
5 | import {
6 | filterByLanguage,
7 | getAllPosts,
8 | sortMDByDate,
9 | sortMDByPinned,
10 | } from "@/utils";
11 | import type { CollectionEntry } from "astro:content";
12 |
13 | const MAX_POSTS = 10;
14 | const allPosts = await getAllPosts();
15 | const allPostsByDate = sortMDByDate(allPosts);
16 | const allPostsByPinned = sortMDByPinned(allPostsByDate);
17 | const filteredPostsByLanguage = filterByLanguage(allPostsByPinned, "es").slice(0, MAX_POSTS);
18 |
19 | const tech: Array<{ title: string; desc: string; href: string }> = [
20 | {
21 | title: "Astro",
22 | desc: "Crea sitios web rápidos, más rápido.",
23 | href: "https://astro.build",
24 | },
25 | {
26 | title: "Astro Assets",
27 | desc: "Compatibilidad con activos optimizados integrados.",
28 | href: "https://docs.astro.build/en/guides/assets/",
29 | },
30 | {
31 | title: "Tailwind CSS",
32 | desc: "Crea sitios web modernos rápidamente sin tener que abandonar el HTML.",
33 | href: "https://tailwindcss.com",
34 | },
35 | {
36 | title: "Markdown",
37 | desc: "Lenguaje de marcado simple y fácil de usar.",
38 | href: "https://www.markdownguide.org/",
39 | },
40 | {
41 | title: "MDX",
42 | desc: "Markdown para la era de los componentes.",
43 | href: "https://mdxjs.com/",
44 | },
45 | {
46 | title: "Astro Icon",
47 | desc: "Un componente de ícono fácil de usar para Astro.",
48 | href: "https://github.com/natemoo-re/astro-icon#readme",
49 | },
50 | ];
51 | ---
52 |
53 |
54 |
55 | Hello World
56 |
57 | ¡Hola a todos! Este es un tema para Astro. Pueden usarlo como blog
58 | personal o portafolio. Si quieren saber más sobre cómo
59 | personalizarlo y hacerlo suyo, hagan clic en el enlace del ícono de
60 | Github que aparece a continuación y los llevará al repositorio.
61 |
62 |
63 | Puedes encontrarme en:
64 |
65 |
66 |
67 |
68 |
69 | Posts
70 |
71 | {
72 | filteredPostsByLanguage.map((p) => (
73 | } />
74 | ))
75 | }
76 |
77 |
78 |
79 |
80 | Technologies
81 |
82 | {
83 | tech.map(({ href, title, desc }) => (
84 |
85 |
91 | {title}
92 |
93 | : {desc}
94 |
95 | ))
96 | }
97 |
98 |
99 |
100 |
101 |
102 | Esto fue traducido usando Google Translate
103 |
104 |
105 |
--------------------------------------------------------------------------------
/src/content/blog/second-post.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Second post'
3 | description: 'Lorem ipsum dolor sit amet'
4 | pubDate: 'Jul 22 2022'
5 | heroImage:
6 | src: '/blog-placeholder-4.jpg'
7 | alt: ''
8 | tags: ["test"]
9 | series: "example"
10 | ---
11 |
12 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet.
13 |
14 | Morbi tristique senectus et netus. Id semper risus in hendrerit gravida rutrum quisque non tellus. Habitasse platea dictumst quisque sagittis purus sit amet. Tellus molestie nunc non blandit massa. Cursus vitae congue mauris rhoncus. Accumsan tortor posuere ac ut. Fringilla urna porttitor rhoncus dolor. Elit ullamcorper dignissim cras tincidunt lobortis. In cursus turpis massa tincidunt dui ut ornare lectus. Integer feugiat scelerisque varius morbi enim nunc. Bibendum neque egestas congue quisque egestas diam. Cras ornare arcu dui vivamus arcu felis bibendum. Dignissim suspendisse in est ante in nibh mauris. Sed tempus urna et pharetra pharetra massa massa ultricies mi.
15 |
16 | Mollis nunc sed id semper risus in. Convallis a cras semper auctor neque. Diam sit amet nisl suscipit. Lacus viverra vitae congue eu consequat ac felis donec. Egestas integer eget aliquet nibh praesent tristique magna sit amet. Eget magna fermentum iaculis eu non diam. In vitae turpis massa sed elementum. Tristique et egestas quis ipsum suspendisse ultrices. Eget lorem dolor sed viverra ipsum. Vel turpis nunc eget lorem dolor sed viverra. Posuere ac ut consequat semper viverra nam. Laoreet suspendisse interdum consectetur libero id faucibus. Diam phasellus vestibulum lorem sed risus ultricies tristique. Rhoncus dolor purus non enim praesent elementum facilisis. Ultrices tincidunt arcu non sodales neque. Tempus egestas sed sed risus pretium quam vulputate. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus ornare. Fringilla urna porttitor rhoncus dolor purus non. Amet dictum sit amet justo donec enim.
17 |
18 | Mattis ullamcorper velit sed ullamcorper morbi tincidunt. Tortor posuere ac ut consequat semper viverra. Tellus mauris a diam maecenas sed enim ut sem viverra. Venenatis urna cursus eget nunc scelerisque viverra mauris in. Arcu ac tortor dignissim convallis aenean et tortor at. Curabitur gravida arcu ac tortor dignissim convallis aenean et tortor. Egestas tellus rutrum tellus pellentesque eu. Fusce ut placerat orci nulla pellentesque dignissim enim sit amet. Ut enim blandit volutpat maecenas volutpat blandit aliquam etiam. Id donec ultrices tincidunt arcu. Id cursus metus aliquam eleifend mi.
19 |
20 | Tempus quam pellentesque nec nam aliquam sem. Risus at ultrices mi tempus imperdiet. Id porta nibh venenatis cras sed felis eget velit. Ipsum a arcu cursus vitae. Facilisis magna etiam tempor orci eu lobortis elementum. Tincidunt dui ut ornare lectus sit. Quisque non tellus orci ac. Blandit libero volutpat sed cras. Nec tincidunt praesent semper feugiat nibh sed pulvinar proin gravida. Egestas integer eget aliquet nibh praesent tristique magna.
21 |
--------------------------------------------------------------------------------
/src/content/blog/third-post.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Third post'
3 | description: 'Lorem ipsum dolor sit amet'
4 | pubDate: 'Jul 15 2022'
5 | heroImage:
6 | src: '/blog-placeholder-4.jpg'
7 | alt: 'blog placeholder'
8 | tags: ["test"]
9 | series: "example"
10 | ---
11 |
12 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet.
13 |
14 | Morbi tristique senectus et netus. Id semper risus in hendrerit gravida rutrum quisque non tellus. Habitasse platea dictumst quisque sagittis purus sit amet. Tellus molestie nunc non blandit massa. Cursus vitae congue mauris rhoncus. Accumsan tortor posuere ac ut. Fringilla urna porttitor rhoncus dolor. Elit ullamcorper dignissim cras tincidunt lobortis. In cursus turpis massa tincidunt dui ut ornare lectus. Integer feugiat scelerisque varius morbi enim nunc. Bibendum neque egestas congue quisque egestas diam. Cras ornare arcu dui vivamus arcu felis bibendum. Dignissim suspendisse in est ante in nibh mauris. Sed tempus urna et pharetra pharetra massa massa ultricies mi.
15 |
16 | Mollis nunc sed id semper risus in. Convallis a cras semper auctor neque. Diam sit amet nisl suscipit. Lacus viverra vitae congue eu consequat ac felis donec. Egestas integer eget aliquet nibh praesent tristique magna sit amet. Eget magna fermentum iaculis eu non diam. In vitae turpis massa sed elementum. Tristique et egestas quis ipsum suspendisse ultrices. Eget lorem dolor sed viverra ipsum. Vel turpis nunc eget lorem dolor sed viverra. Posuere ac ut consequat semper viverra nam. Laoreet suspendisse interdum consectetur libero id faucibus. Diam phasellus vestibulum lorem sed risus ultricies tristique. Rhoncus dolor purus non enim praesent elementum facilisis. Ultrices tincidunt arcu non sodales neque. Tempus egestas sed sed risus pretium quam vulputate. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus ornare. Fringilla urna porttitor rhoncus dolor purus non. Amet dictum sit amet justo donec enim.
17 |
18 | Mattis ullamcorper velit sed ullamcorper morbi tincidunt. Tortor posuere ac ut consequat semper viverra. Tellus mauris a diam maecenas sed enim ut sem viverra. Venenatis urna cursus eget nunc scelerisque viverra mauris in. Arcu ac tortor dignissim convallis aenean et tortor at. Curabitur gravida arcu ac tortor dignissim convallis aenean et tortor. Egestas tellus rutrum tellus pellentesque eu. Fusce ut placerat orci nulla pellentesque dignissim enim sit amet. Ut enim blandit volutpat maecenas volutpat blandit aliquam etiam. Id donec ultrices tincidunt arcu. Id cursus metus aliquam eleifend mi.
19 |
20 | Tempus quam pellentesque nec nam aliquam sem. Risus at ultrices mi tempus imperdiet. Id porta nibh venenatis cras sed felis eget velit. Ipsum a arcu cursus vitae. Facilisis magna etiam tempor orci eu lobortis elementum. Tincidunt dui ut ornare lectus sit. Quisque non tellus orci ac. Blandit libero volutpat sed cras. Nec tincidunt praesent semper feugiat nibh sed pulvinar proin gravida. Egestas integer eget aliquet nibh praesent tristique magna.
21 |
--------------------------------------------------------------------------------
/src/content/blog/first-post.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'First post'
3 | description: 'Lorem ipsum dolor sit amet'
4 | pubDate: 'Jul 08 2022'
5 | heroImage:
6 | src: '/blog-placeholder-5.jpg'
7 | alt: 'blog placeholder'
8 | tags: ["placeholder", "test"]
9 | updatedDate: 'Sep 10 2022'
10 | series: "example"
11 | ---
12 |
13 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet.
14 |
15 | Morbi tristique senectus et netus. Id semper risus in hendrerit gravida rutrum quisque non tellus. Habitasse platea dictumst quisque sagittis purus sit amet. Tellus molestie nunc non blandit massa. Cursus vitae congue mauris rhoncus. Accumsan tortor posuere ac ut. Fringilla urna porttitor rhoncus dolor. Elit ullamcorper dignissim cras tincidunt lobortis. In cursus turpis massa tincidunt dui ut ornare lectus. Integer feugiat scelerisque varius morbi enim nunc. Bibendum neque egestas congue quisque egestas diam. Cras ornare arcu dui vivamus arcu felis bibendum. Dignissim suspendisse in est ante in nibh mauris. Sed tempus urna et pharetra pharetra massa massa ultricies mi.
16 |
17 | Mollis nunc sed id semper risus in. Convallis a cras semper auctor neque. Diam sit amet nisl suscipit. Lacus viverra vitae congue eu consequat ac felis donec. Egestas integer eget aliquet nibh praesent tristique magna sit amet. Eget magna fermentum iaculis eu non diam. In vitae turpis massa sed elementum. Tristique et egestas quis ipsum suspendisse ultrices. Eget lorem dolor sed viverra ipsum. Vel turpis nunc eget lorem dolor sed viverra. Posuere ac ut consequat semper viverra nam. Laoreet suspendisse interdum consectetur libero id faucibus. Diam phasellus vestibulum lorem sed risus ultricies tristique. Rhoncus dolor purus non enim praesent elementum facilisis. Ultrices tincidunt arcu non sodales neque. Tempus egestas sed sed risus pretium quam vulputate. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus ornare. Fringilla urna porttitor rhoncus dolor purus non. Amet dictum sit amet justo donec enim.
18 |
19 | Mattis ullamcorper velit sed ullamcorper morbi tincidunt. Tortor posuere ac ut consequat semper viverra. Tellus mauris a diam maecenas sed enim ut sem viverra. Venenatis urna cursus eget nunc scelerisque viverra mauris in. Arcu ac tortor dignissim convallis aenean et tortor at. Curabitur gravida arcu ac tortor dignissim convallis aenean et tortor. Egestas tellus rutrum tellus pellentesque eu. Fusce ut placerat orci nulla pellentesque dignissim enim sit amet. Ut enim blandit volutpat maecenas volutpat blandit aliquam etiam. Id donec ultrices tincidunt arcu. Id cursus metus aliquam eleifend mi.
20 |
21 | Tempus quam pellentesque nec nam aliquam sem. Risus at ultrices mi tempus imperdiet. Id porta nibh venenatis cras sed felis eget velit. Ipsum a arcu cursus vitae. Facilisis magna etiam tempor orci eu lobortis elementum. Tincidunt dui ut ornare lectus sit. Quisque non tellus orci ac. Blandit libero volutpat sed cras. Nec tincidunt praesent semper feugiat nibh sed pulvinar proin gravida. Egestas integer eget aliquet nibh praesent tristique magna.
22 |
--------------------------------------------------------------------------------
/src/pages/[lang]/blog/pages/[page].astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { Page, GetStaticPaths } from "astro";
3 | import type { CollectionEntry } from "astro:content";
4 | import BaseLayout from "@/layouts/Sidebar";
5 | import PostPreview from "@/components/blog/PostPreviewA";
6 | import Pagination from "@/components/Pagination";
7 | import {
8 | getAllPosts,
9 | getUniqueTags,
10 | sortMDByDate,
11 | sortMDByPinned,
12 | filterByLanguage,
13 | getSupportedLanguages,
14 | type SupportedLanguage,
15 | } from "@/utils";
16 | import { siteConfig } from "src/consts";
17 |
18 | export const getStaticPaths = (async ({ paginate }) => {
19 | // grab all posts besides drafts and hidden ones
20 | const allPosts = await getAllPosts(true);
21 | const allPostsByDate = sortMDByDate(allPosts);
22 | const allPostsByPinned = sortMDByPinned(allPostsByDate);
23 | const tags = getUniqueTags(allPosts);
24 |
25 | // filter posts by language and paginate
26 | return getSupportedLanguages().flatMap((lang) => {
27 | const filteredByLang = filterByLanguage(
28 | allPostsByPinned,
29 | lang as SupportedLanguage,
30 | );
31 |
32 | return paginate(filteredByLang, {
33 | params: { lang },
34 | props: { tags },
35 | pageSize: siteConfig.settings.paginationSize,
36 | });
37 | });
38 | }) satisfies GetStaticPaths;
39 |
40 | interface Props {
41 | page: Page>;
42 | tags: string[];
43 | }
44 |
45 | const { page, tags } = Astro.props;
46 |
47 | const meta = {
48 | title: "Posts",
49 | description: "Read my collection of posts and the things that interest me",
50 | };
51 |
52 | const paginationProps = {
53 | ...(page.url.prev && {
54 | prevUrl: {
55 | url: page.url.prev,
56 | text: `← Previous Posts`,
57 | },
58 | }),
59 | ...(page.url.next && {
60 | nextUrl: {
61 | url: page.url.next,
62 | text: `Next Posts →`,
63 | },
64 | }),
65 | };
66 | ---
67 |
68 |
69 |
70 | Posts
71 |
72 | {page.data.map((post) => )}
73 |
74 |
75 |
76 | {
77 | !!tags.length && (
78 |
115 | )
116 | }
117 |
118 |
--------------------------------------------------------------------------------
/src/components/Search.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import "@/styles/search";
3 | import { Icon } from "astro-icon";
4 | ---
5 |
6 |
7 |
13 |
18 |
19 |
25 |
33 |
34 |
35 |
36 |
120 |
--------------------------------------------------------------------------------
/src/layouts/Project.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { siteConfig } from "../consts";
3 | import SidebarLayout from "./Sidebar.astro";
4 | import SocialMediaLinks from "@/components/SocialMediaLinks";
5 | import TOC from "@/components/TableOfContents";
6 | import { getLangFromUrl, useUITranslations } from "@/utils";
7 | import { Icon } from "astro-icon";
8 | import type { CollectionEntry } from "astro:content";
9 |
10 | interface Props {
11 | project: CollectionEntry<"project">;
12 | }
13 |
14 | const { project } = Astro.props;
15 | const lang = getLangFromUrl(Astro.url);
16 | const translate = useUITranslations(lang);
17 | const {
18 | data: {
19 | title,
20 | pubDate,
21 | description,
22 | heroImage,
23 | platform,
24 | stack,
25 | website,
26 | github,
27 | },
28 | } = project;
29 | const { headings } = project.render && (await project.render());
30 | const articleDate = pubDate.toISOString();
31 | ---
32 |
33 |
34 | {title}
35 |
36 | {
37 | heroImage && (
38 |
39 |
40 |
41 | )
42 | }
43 |
44 |
45 | {
46 | platform && (
47 |
48 | {translate("project.platform").text}
49 | {platform}
50 |
51 | )
52 | }
53 | {
54 | stack && (
55 |
56 | {translate("project.stack").text}
57 |
58 | {stack.map((s: string) => (
59 |
60 |
66 |
67 | ))}
68 |
69 |
70 | )
71 | }
72 | {
73 | website && (
74 |
75 | {translate("project.website").text}
76 |
77 | {website}
78 |
79 |
80 | )
81 | }
82 | {
83 | github && (
84 |
85 | GITHUB
86 |
87 | {github}
88 |
89 |
90 | )
91 | }
92 |
93 |
94 |
95 |
96 |
99 |
100 |
101 |
102 |
103 |
104 |
105 | {siteConfig.profile.author}
106 |
107 | {
108 | siteConfig.profile.description && (
109 |
{translate("profile.description").text}
110 | )
111 | }
112 |
113 |
---
114 |
115 | {description}
116 |
117 |
118 | {!!headings.length && }
119 |
120 |
121 |
122 |
131 |
--------------------------------------------------------------------------------
/src/styles/global.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | color-scheme: light;
8 | /* https://tailwindcss.com/docs/customizing-colors#using-css-variables */
9 | --theme-bg: #efefef;
10 | --theme-text: #222222;
11 | --theme-accent: #dd7878;
12 | --theme-accent-2: #bcc0cd;
13 | --theme-surface: #e6e6e6;
14 | --theme-quote: #dd7878;
15 | --theme-highlight: #dd7878;
16 | }
17 |
18 | :root.dark {
19 | color-scheme: dark;
20 | --theme-bg: #1d1d1d;
21 | --theme-text: #ededed;
22 | --theme-accent: #93e1d4;
23 | --theme-accent-2: #545454;
24 | --theme-surface: #2f2f2f;
25 | --theme-quote: #93e1d4;
26 | --theme-highlight: #93e1d4;
27 | }
28 |
29 | html {
30 | @apply scroll-smooth;
31 | }
32 |
33 | html body {
34 | @apply mx-auto flex min-h-screen max-w-6xl flex-col px-4 pt-4 md:pt-8 font-mono text-sm font-normal antialiased bg-bgColor text-textColor;
35 | }
36 |
37 | /*
38 | * CSS for markdown and MDX blog posts
39 | */
40 |
41 | .prose-cody blockquote {
42 | @apply text-quote border-l-0;
43 | }
44 |
45 | .prose-cody a {
46 | @apply no-underline;
47 | }
48 |
49 | .prose-cody strong {
50 | font-weight: 700;
51 | }
52 |
53 | .prose-cody :where(code):not(:where([class~=not-prose],[class~=not-prose] *)) {
54 | @apply bg-accent !text-surface px-1 py-0;
55 | }
56 |
57 | .prose :where(code):not(:where([class~="not-prose"],[class~="not-prose"] *))::before{
58 | content: none !important;
59 | }
60 |
61 | .prose :where(code):not(:where([class~="not-prose"],[class~="not-prose"] *))::after{
62 | content: none !important;
63 | }
64 |
65 | .prose-cody hr {
66 | border-top-style: dashed;
67 | border-top-width: 0.5px;
68 | @apply border-accent my-4;
69 | }
70 | .prose-cody thead {
71 | @apply border-b-0;
72 | }
73 |
74 | .prose-cody thead th {
75 | @apply font-bold border-b border-dashed border-accent;
76 | }
77 |
78 | .prose-cody tbody tr {
79 | @apply border-b-0;
80 | }
81 |
82 | .prose-cody tfoot {
83 | @apply font-bold border-t border-dashed border-accent;
84 | }
85 |
86 | .prose-cody sup {
87 | @apply ms-0.5;
88 | }
89 |
90 | .prose-cody sup > a {
91 | @apply bg-none;
92 | }
93 |
94 | .prose-cody sup > a:hover {
95 | @apply text-accent no-underline bg-none;
96 | }
97 |
98 | .prose-cody sup > a:before {
99 | content: "[";
100 | @apply text-accent;
101 | }
102 |
103 | .prose-cody sup > a:after {
104 | content: "]";
105 | @apply text-accent;
106 | }
107 |
108 | .prose-cody mark {
109 | @apply bg-accent;
110 | }
111 |
112 | .prose-cody li::marker {
113 | @apply text-accent;
114 | }
115 |
116 | .prose-cody table {
117 | display: block;
118 | overflow-x: auto;
119 | white-space: nowrap;
120 | }
121 |
122 | /*
123 | * ===== END =====
124 | */
125 |
126 | .tag {
127 | @apply bg-accent px-1 py-0 text-bgColor font-semibold text-sm;
128 | }
129 |
130 | .tag:before {
131 | content: "#";
132 | @apply -mr-2;
133 | }
134 |
135 | .cody-link {
136 | @apply bg-[size:100%_6px] bg-bottom bg-repeat-x w-fit;
137 | background-image: linear-gradient(
138 | transparent,
139 | transparent 5px,
140 | var(--theme-text) 5px,
141 | var(--theme-text)
142 | );
143 | }
144 |
145 | .cody-link:hover {
146 | background-image: linear-gradient(
147 | transparent,
148 | transparent 4px,
149 | var(--theme-accent) 4px,
150 | var(--theme-accent)
151 | );
152 | }
153 |
154 | .cody-bg {
155 | @apply p-4 bg-surface;
156 | }
157 |
158 | .cody-copy-code {
159 | @apply absolute top-2 right-2 bg-accent text-bgColor p-0.5 border border-accent hover:bg-bgColor hover:text-accent transition-all duration-200 text-sm;
160 | }
161 |
162 | .title:before {
163 | content: "#";
164 | @apply mr-2 text-accent-2;
165 | }
166 |
167 | .title {
168 | @apply text-lg font-bold mb-4;
169 | }
170 |
171 | hr {
172 | @apply border-accent border-dashed my-6;
173 | }
174 |
175 | ::selection {
176 | @apply bg-highlight text-bgColor;
177 | }
178 |
179 | ::-moz-selection {
180 | @apply bg-highlight text-bgColor;
181 | }
182 |
183 | .fade-in {
184 | @apply transition-opacity duration-700 ease-in opacity-100 duration-700;
185 | }
186 |
187 | .fade-out {
188 | @apply transition-opacity duration-700 ease-in opacity-0 duration-700;
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/src/layouts/BlogPost.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { CollectionEntry } from "astro:content";
3 | import Layout from "../layouts/Sidebar.astro";
4 | import FormattedDate from "../components/FormattedDate.astro";
5 | import SocialMediaLinks from "@/components/SocialMediaLinks";
6 | import TOC from "@/components/TableOfContents";
7 | import BackToTopBtn from "@/components/BackToTopBtn";
8 | import CopyBtn from "@/components/CopyBtn";
9 | import { Icon } from "astro-icon";
10 | import { siteConfig } from "../consts";
11 | import { getLangFromUrl, useUITranslations } from "@/utils";
12 |
13 | interface Props {
14 | post: CollectionEntry<"blog">;
15 | }
16 |
17 | const { post } = Astro.props;
18 |
19 | const {
20 | slug,
21 | data: {
22 | title,
23 | description,
24 | pubDate,
25 | updatedDate,
26 | heroImage,
27 | ogImage,
28 | tags,
29 | series,
30 | draft,
31 | },
32 | } = post;
33 |
34 | const { headings, remarkPluginFrontmatter } =
35 | post.render && (await post.render());
36 | const { minutesRead } = remarkPluginFrontmatter;
37 | const lang = getLangFromUrl(Astro.url);
38 | const translate = useUITranslations(lang);
39 |
40 | const socialImage = ogImage ?? `/og-image/${slug}.png`;
41 | const dateTimeOptions: Intl.DateTimeFormatOptions = {
42 | year: "numeric",
43 | month: "long",
44 | day: "numeric",
45 | };
46 |
47 | const articleDate = updatedDate?.toISOString() ?? pubDate.toISOString();
48 | ---
49 |
50 |
51 |
52 |
122 |
123 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 | {siteConfig.profile.author}
134 |
135 | {
136 | siteConfig.profile.description && (
137 |
{translate("profile.description").text}
138 | )
139 | }
140 |
141 |
---
142 |
143 | {description}
144 |
145 | {
146 | series && (
147 |
148 |
Part of series:
149 |
150 |
160 |
161 | )
162 | }
163 |
164 | {!!headings.length && }
165 |
166 |
167 |
168 |
169 |
--------------------------------------------------------------------------------
/src/components/Header.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Search from './Search.astro';
3 | import HeaderLink from "./HeaderLink.astro";
4 | import { siteConfig, NAV_LINKS } from "../consts";
5 | import ThemeToggle from "@/components/ThemeToggle";
6 | import SelectLanguage from "@/components/SelectLanguage";
7 | import { getLangFromUrl } from "@/utils";
8 | import { useNavTranslations } from "src/utils/i18n";
9 | import type { NavEntry } from "@/i18n/nav";
10 |
11 | const url = new URL(Astro.request.url);
12 | const lang = getLangFromUrl(Astro.url);
13 | const t = useNavTranslations(lang);
14 |
15 | function buildNavLink(nav: string, entry: NavEntry): string {
16 | if (entry.route) {
17 | return `/${lang}${entry.route}`;
18 | }
19 |
20 | return `/${lang}${nav != "home" ? `/${entry.slug}/` : "/"}`;
21 | }
22 | ---
23 |
24 |
28 |
36 |
37 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 | open main menu
90 |
129 |
130 |
131 |
132 |
133 |
134 |
150 |
151 |
185 |
--------------------------------------------------------------------------------
/src/content/blog/markdown-style-guide.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Markdown Style Guide'
3 | description: 'Here is a sample of some basic Markdown syntax that can be used when writing Markdown content in Astro.'
4 | pubDate: 'Jul 01 2022'
5 | heroImage:
6 | src: '/blog-placeholder-1.jpg'
7 | alt: ''
8 | order: 1
9 | tags: ["guide"]
10 | ---
11 |
12 | Here is a sample of some basic Markdown syntax that can be used when writing Markdown content in Astro.
13 |
14 | ## Headings
15 |
16 | The following HTML ``—`` elements represent six levels of section headings. `` is the highest section level while `` is the lowest.
17 |
18 | # H1
19 |
20 | ## H2
21 |
22 | ### H3
23 |
24 | #### H4
25 |
26 | ##### H5
27 |
28 | ###### H6
29 |
30 | ## Paragraph
31 |
32 | Xerum, quo qui aut unt expliquam qui dolut labo. Aque venitatiusda cum, voluptionse latur sitiae dolessi aut parist aut dollo enim qui voluptate ma dolestendit peritin re plis aut quas inctum laceat est volestemque commosa as cus endigna tectur, offic to cor sequas etum rerum idem sintibus eiur? Quianimin porecus evelectur, cum que nis nust voloribus ratem aut omnimi, sitatur? Quiatem. Nam, omnis sum am facea corem alique molestrunt et eos evelece arcillit ut aut eos eos nus, sin conecerem erum fuga. Ri oditatquam, ad quibus unda veliamenimin cusam et facea ipsamus es exerum sitate dolores editium rerore eost, temped molorro ratiae volorro te reribus dolorer sperchicium faceata tiustia prat.
33 |
34 | Itatur? Quiatae cullecum rem ent aut odis in re eossequodi nonsequ idebis ne sapicia is sinveli squiatum, core et que aut hariosam ex eat.
35 |
36 | ## Images
37 |
38 | #### Syntax
39 |
40 | ```markdown
41 | 
42 | ```
43 |
44 | #### Output
45 |
46 | 
47 |
48 | ## Blockquotes
49 |
50 | The blockquote element represents content that is quoted from another source, optionally with a citation which must be within a `footer` or `cite` element, and optionally with in-line changes such as annotations and abbreviations.
51 |
52 | ### Blockquote without attribution
53 |
54 | #### Syntax
55 |
56 | ```markdown
57 | > Tiam, ad mint andaepu dandae nostion secatur sequo quae.
58 | > **Note** that you can use _Markdown syntax_ within a blockquote.
59 | ```
60 |
61 | #### Output
62 |
63 | > Tiam, ad mint andaepu dandae nostion secatur sequo quae.
64 | > **Note** that you can use _Markdown syntax_ within a blockquote.
65 |
66 | ### Blockquote with attribution
67 |
68 | #### Syntax
69 |
70 | ```markdown
71 | > Don't communicate by sharing memory, share memory by communicating.
72 | > — Rob Pike[^1]
73 | ```
74 |
75 | #### Output
76 |
77 | > Don't communicate by sharing memory, share memory by communicating.
78 | > — Rob Pike[^1]
79 |
80 | [^1]: The above quote is excerpted from Rob Pike's [talk](https://www.youtube.com/watch?v=PAAkCSZUG1c) during Gopherfest, November 18, 2015.
81 |
82 | ## Tables
83 |
84 | #### Syntax
85 |
86 | ```markdown
87 | | Italics | Bold | Code |
88 | | --------- | -------- | ------ |
89 | | _italics_ | **bold** | `code` |
90 | ```
91 |
92 | #### Output
93 |
94 | | Italics | Bold | Code |
95 | | --------- | -------- | ------ |
96 | | _italics_ | **bold** | `code` |
97 |
98 | ## Code Blocks
99 |
100 | #### Syntax
101 |
102 | we can use 3 backticks ``` in new line and write snippet and close with 3 backticks on new line and to highlight language specific syntac, write one word of language name after first 3 backticks, for eg. html, javascript, css, markdown, typescript, txt, bash
103 |
104 | ````markdown
105 | ```html
106 |
107 |
108 |
109 |
110 | Example HTML5 Document
111 |
112 |
113 | Test
114 |
115 |
116 | ```
117 | ````
118 |
119 | Output
120 |
121 | ```html
122 |
123 |
124 |
125 |
126 | Example HTML5 Document
127 |
128 |
129 | Test
130 |
131 |
132 | ```
133 |
134 | ## List Types
135 |
136 | ### Ordered List
137 |
138 | #### Syntax
139 |
140 | ```markdown
141 | 1. First item
142 | 2. Second item
143 | 3. Third item
144 | ```
145 |
146 | #### Output
147 |
148 | 1. First item
149 | 2. Second item
150 | 3. Third item
151 |
152 | ### Unordered List
153 |
154 | #### Syntax
155 |
156 | ```markdown
157 | - List item
158 | - Another item
159 | - And another item
160 | ```
161 |
162 | #### Output
163 |
164 | - List item
165 | - Another item
166 | - And another item
167 |
168 | ### Nested list
169 |
170 | #### Syntax
171 |
172 | ```markdown
173 | - Fruit
174 | - Apple
175 | - Orange
176 | - Banana
177 | - Dairy
178 | - Milk
179 | - Cheese
180 | ```
181 |
182 | #### Output
183 |
184 | - Fruit
185 | - Apple
186 | - Orange
187 | - Banana
188 | - Dairy
189 | - Milk
190 | - Cheese
191 |
192 | ## Other Elements — abbr, sub, sup, kbd, mark
193 |
194 | #### Syntax
195 |
196 | ```markdown
197 | GIF is a bitmap image format.
198 |
199 | H2 O
200 |
201 | Xn + Yn = Zn
202 |
203 | Press CTRL +ALT +Delete to end the session.
204 |
205 | Most salamanders are nocturnal, and hunt for insects, worms, and other small creatures.
206 | ```
207 |
208 | #### Output
209 |
210 | GIF is a bitmap image format.
211 |
212 | H2 O
213 |
214 | Xn + Yn = Zn
215 |
216 | Press CTRL +ALT +Delete to end the session.
217 |
218 | Most salamanders are nocturnal, and hunt for insects, worms, and other small creatures.
219 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | [![Contributors][contributors-shield]][contributors-url]
5 | [![Forks][forks-shield]][forks-url]
6 | [![Stargazers][stars-shield]][stars-url]
7 | [![Issues][issues-shield]][issues-url]
8 | [![MIT License][license-shield]][license-url]
9 | [](https://app.netlify.com/sites/astro-theme-cody/deploys)
10 |
11 |
12 |
13 |
14 |
15 |
Astro Theme Cody
16 |
17 | A minimalist blog theme built with Astro. A quick and easy starter build for anyone who wants to start their own blog.
18 |
19 |
View Demo
20 | ·
21 |
Report Bug
22 | ·
23 |
Request Feature
24 |
25 |
26 |
27 |
28 | 
29 |
30 |
31 | ## Table of Contents
32 | - [Motivation](#motivation)
33 | - [Key Features](#key-features)
34 | - [Demo](#demo)
35 | - [Quick Start](#quick-start)
36 | - [Commands](#commands)
37 | - [Configuring Your Website](#configuring-your-website)
38 | - [Site Config, Social Media Links and Navbar Links](#site-config-social-media-links-and-navbar-links)
39 | - [Adding Posts and Projects](#adding-posts-and-projects)
40 | - [Multi-Language Support](#multi-language-support)
41 | - [Deploy](#deploy)
42 | - [Extra Functionality](#extra-funtionality)
43 | - [Credit](#credit)
44 |
45 | ## Motivation
46 | Although there are already so many blog themes out there, I wanted one that would give off
47 | that terminal/dev vibe and a few features that other themes did not have.
48 |
49 | ## Key Features:
50 | - Astro v4 Fast 🚀
51 | - TailwindCSS Utility classes
52 | - Accessible, semantic HTML markup
53 | - Responsive & SEO-friendly
54 | - Multi-language support
55 | - Dark / Light mode, using Tailwind and CSS variables
56 | - Pinned posts and projects
57 | - [Astro Assets Integration](https://docs.astro.build/en/guides/assets/) for optimised images
58 | - MD & [MDX](https://docs.astro.build/en/guides/markdown-content/#mdx-only-features) posts
59 | - Pagination
60 | - Search through posts with [PageFind](https://pagefind.app/)
61 | - [Automatic RSS feed](https://docs.astro.build/en/guides/rss)
62 | - Auto-generated [sitemap](https://docs.astro.build/en/guides/integrations-guide/sitemap/)
63 | - [Astro Icon](https://github.com/natemoo-re/astro-icon) svg icon component
64 | - Back To Top button
65 |
66 | ## Demo
67 | Check out the demo [here](https://astro-theme-cody.netlify.app).
68 |
69 | ## Quick Start
70 | [Create a new repo](https://github.com/new?template_name=astro-theme-cody&template_owner=kirontoo) from this template.
71 |
72 | ```
73 | # npm 7+
74 | npm create astro@latest -- --template kirontoo/astro-theme-cody
75 |
76 | # pnpm
77 | pnpm dlx create-astro --template kirontoo/astro-theme-cody
78 | ```
79 |
80 | (back to top )
81 |
82 |
83 | ## Commands
84 |
85 | All commands are run from the root of the project, from a terminal:
86 |
87 | | Command | Action |
88 | | :------------------------ | :----------------------------------------------- |
89 | | `pnpm install` | Installs dependencies |
90 | | `pnpm run dev` | Starts local dev server at `localhost:4321` |
91 | | `pnpm run build` | Build your production site to `./dist/` |
92 | | `pnpm run preview` | Preview your build locally, before deploying |
93 | | `pnpm run astro ...` | Run CLI commands like `astro add`, `astro check` |
94 | | `pnpm run astro -- --help` | Get help using the Astro CLI |
95 |
96 | (back to top )
97 |
98 | ## Configuring Your Website
99 |
100 | ### Site Config, Social Media Links and Navbar Links
101 | Your site config, social media links and Navbar links are all placed within [`src/consts.ts`](https://github.com/kirontoo/astro-theme-cody/blob/main/src/consts.ts).
102 | There you can configure your site default title, description, language, profile, social media links and visible nav bar links.
103 |
104 | ### Adding Posts and Projects
105 | This theme utilises [ Content Collections ](https://docs.astro.build/en/guides/content-collections/)
106 | to organise Markdown and/or MDX files, as well as type-checking frontmatter
107 | with a schema -> `src/content/config.ts`.
108 |
109 | You can find examples of blog posts in `src/content/blog` and examples of projects in `src/content/project`.
110 |
111 | #### Blog post frontmatter
112 | | Property (* = required) | Description |
113 | |:-----------------------| :---------- |
114 | | title * | Title of your post. Limited to a maximum of 150 characters |
115 | | description * | Short description of your post, which will also be used for SEO. Has a max length of 250 characters. |
116 | | pubDate * | Published date |
117 | | updateDate | Optional date representing when the post has been updated. Note that this date will be used instead of the published date to order posts when available.|
118 | | heroImage | Optional cover image for you post. Include both a `src` and a `alt` property. Check `src/content/blog/first-post.md` for an example.|
119 | | ogImage | Optional image used for SEO. |
120 | | tags | Use optional tags to organize your posts into categories or topics. All tags will be shown in `yourdomain.com/tags`. |
121 | | series | Use optional series to organize your posts into a series of posts. All series will be shown in `yourdomain.com/series`. |
122 | | draft | Optional boolean. Removes posts from being published.|
123 | | order | Optional number value from 1 - 5 to pin certain posts to the top. Limited to only 5 pinned posts|
124 | | hide | Optional boolean. Hide a post on `/blog` page. Will still show up in `/archive`|
125 |
126 | #### Project frontmatter
127 |
128 | | Property (* = required) | Description |
129 | |:-----------------------| :----------|
130 | | title * | Title of your project. Limited to a maximum of 150 characters |
131 | | description * | Short description of your project, which will also be used for SEO.|
132 | | pubDate * | Published date |
133 | | heroImage | Optional cover image for you project. Include both a `src` and a `alt` property. Check `src/pages/projects/project-1.md` for an example.|
134 | | ogImage | Optional image used for SEO. |
135 | | stack * | A list of technologies your project used which will be rendered as icons. This theme uses the SVG Logos librar, you can use [icones.js.org](https://icones.js.org/collection/vscode-icons) to find the icons you need.|
136 | | platform | Link to website or demo |
137 | | website | Link to website or demo |
138 | | github | Your github repo link |
139 | | draft | Optional boolean. Removes projects from being published.|
140 | | order | Optional number value from 1 - 5 to pin certain projects to the top. Limited to only 5 pinned posts|
141 |
142 | (back to top )
143 |
144 | ### Multi-Language Support
145 | See the [wiki](https://github.com/kirontoo/astro-theme-cody/wiki/Set-up-multi%E2%80%90language-support) to set up multi-language support for your blog.
146 |
147 | ## Deploy
148 | Astro provides great documentation for deploying your Astro websites on various platforms.
149 | You can find it [here](https://docs.astro.build/en/guides/deploy/).
150 |
151 | ## Extra Funtionality
152 | Check out [Astro Recipes](https://docs.astro.build/en/recipes/) to add other functionality like a RSS feed.
153 |
154 |
155 | ## Credit
156 |
157 | This theme is based off of the theme [Astro Cactus](https://astro-theme-cactus.netlify.app) and [Hugo Risotto](https://risotto.joeroe.io).
158 |
159 | (back to top )
160 |
161 | [contributors-shield]: https://img.shields.io/github/contributors/kirontoo/astro-theme-cody.svg?style=for-the-badge
162 | [contributors-url]: https://github.com/kirontoo/astro-theme-cody/graphs/contributors
163 | [forks-shield]: https://img.shields.io/github/forks/kirontoo/astro-theme-cody.svg?style=for-the-badge
164 | [forks-url]: https://github.com/kirontoo/astro-theme-cody/network/members
165 | [stars-shield]: https://img.shields.io/github/stars/kirontoo/astro-theme-cody.svg?style=for-the-badge
166 | [stars-url]: https://github.com/kirontoo/astro-theme-cody/stargazers
167 | [issues-shield]: https://img.shields.io/github/issues/kirontoo/astro-theme-cody.svg?style=for-the-badge
168 | [issues-url]: https://github.com/kirontoo/astro-theme-cody/issues
169 | [license-shield]: https://img.shields.io/github/license/kirontoo/astro-theme-cody.svg?style=for-the-badge
170 | [license-url]: https://github.com/kirontoo/astro-theme-cody/blob/master/LICENSE.txt
171 |
--------------------------------------------------------------------------------