├── public ├── favicon.ico ├── favicon-192.png ├── favicon-512.png ├── favicon-dev.ico ├── favicon-dev-192.png ├── favicon-dev-512.png ├── apple-touch-icon.png ├── apple-touch-icon-dev.png ├── blog-placeholder-1.jpg ├── blog-placeholder-2.jpg ├── blog-placeholder-3.jpg ├── blog-placeholder-4.jpg ├── blog-placeholder-5.jpg ├── blog-placeholder-about.jpg ├── manifest.webmanifest ├── favicon-dev.svg └── favicon.svg ├── src ├── env.d.ts ├── pages │ ├── de │ │ ├── blog.md │ │ ├── thema.md │ │ └── index.md │ ├── blog.md │ ├── nl │ │ ├── blog.md │ │ ├── onderwerp.md │ │ ├── index.md │ │ └── over.mdx │ ├── tag.md │ ├── index.md │ ├── rss-de.xml.ts │ ├── rss-en.xml.ts │ ├── rss-nl.xml.ts │ ├── rss.xml.ts │ ├── about.mdx │ ├── [...blogPost].astro │ ├── [...person].astro │ ├── [...blogPostsPerTag].astro │ └── 404.astro ├── types.ts ├── components │ ├── PeopleList.astro │ ├── FormattedDate.astro │ ├── TranslationsBanner.astro │ ├── PostTags.astro │ ├── HeaderLink.astro │ ├── DescriptionMeta.astro │ ├── TranslationLinks.astro │ ├── Byline.astro │ ├── Header.astro │ ├── SkipLink.astro │ ├── Html.astro │ ├── Head.astro │ └── MainI18n.astro ├── consts.ts ├── utilities │ ├── getPageNumbers.ts │ ├── getPagePath.ts │ ├── getPostsToRenderInRSS.ts │ ├── getPublishedPosts.ts │ ├── getPostPath.ts │ ├── getPagination.ts │ ├── people.ts │ ├── getPageDescription.ts │ └── tags.ts ├── styles │ ├── prose.css │ └── global.css ├── layouts │ ├── Page.astro │ ├── 404.astro │ ├── TagArchive.astro │ ├── PostsPerTag.astro │ ├── Person.astro │ ├── Post.astro │ └── Archive.astro ├── header.ts ├── content │ ├── config.ts │ └── blog │ │ ├── nl │ │ ├── tweede-post.md │ │ └── derde-post.md │ │ ├── de │ │ ├── erster-post.md │ │ └── zweiter-post.md │ │ ├── second-post.mdx │ │ ├── first-post.md │ │ └── third-post.md ├── i18n │ ├── utilities.ts │ ├── i18n.ts │ └── uiStrings.ts └── people.ts ├── .vscode ├── extensions.json └── launch.json ├── .gitignore ├── tsconfig.json ├── package.json ├── astro.config.mjs └── README.md /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kslstn/astro-i18n-blog-starter/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /public/favicon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kslstn/astro-i18n-blog-starter/HEAD/public/favicon-192.png -------------------------------------------------------------------------------- /public/favicon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kslstn/astro-i18n-blog-starter/HEAD/public/favicon-512.png -------------------------------------------------------------------------------- /public/favicon-dev.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kslstn/astro-i18n-blog-starter/HEAD/public/favicon-dev.ico -------------------------------------------------------------------------------- /public/favicon-dev-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kslstn/astro-i18n-blog-starter/HEAD/public/favicon-dev-192.png -------------------------------------------------------------------------------- /public/favicon-dev-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kslstn/astro-i18n-blog-starter/HEAD/public/favicon-dev-512.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kslstn/astro-i18n-blog-starter/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/apple-touch-icon-dev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kslstn/astro-i18n-blog-starter/HEAD/public/apple-touch-icon-dev.png -------------------------------------------------------------------------------- /public/blog-placeholder-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kslstn/astro-i18n-blog-starter/HEAD/public/blog-placeholder-1.jpg -------------------------------------------------------------------------------- /public/blog-placeholder-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kslstn/astro-i18n-blog-starter/HEAD/public/blog-placeholder-2.jpg -------------------------------------------------------------------------------- /public/blog-placeholder-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kslstn/astro-i18n-blog-starter/HEAD/public/blog-placeholder-3.jpg -------------------------------------------------------------------------------- /public/blog-placeholder-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kslstn/astro-i18n-blog-starter/HEAD/public/blog-placeholder-4.jpg -------------------------------------------------------------------------------- /public/blog-placeholder-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kslstn/astro-i18n-blog-starter/HEAD/public/blog-placeholder-5.jpg -------------------------------------------------------------------------------- /public/blog-placeholder-about.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kslstn/astro-i18n-blog-starter/HEAD/public/blog-placeholder-about.jpg -------------------------------------------------------------------------------- /src/pages/de/blog.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: '@layouts/Archive.astro' 3 | title: Blog 4 | collection: blog 5 | reference: blog 6 | --- 7 | Hier ist der Blog. -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode", "unifiedjs.vscode-mdx"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /src/pages/blog.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: '@layouts/Archive.astro' 3 | title: Blog 4 | description: '' 5 | collection: blog 6 | reference: blog 7 | --- 8 | Here's the blog archive. -------------------------------------------------------------------------------- /src/pages/nl/blog.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: '@layouts/Archive.astro' 3 | title: Blog 4 | description: '' 5 | collection: blog 6 | reference: blog 7 | --- 8 | Hier zijn de Nederlandse posts. -------------------------------------------------------------------------------- /src/pages/tag.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: '@layouts/TagArchive.astro' 3 | title: All tags 4 | collection: blog 5 | reference: tagArchive 6 | --- 7 | Here are all topics discussed in blog posts on this website. -------------------------------------------------------------------------------- /public/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "icons": [ 3 | { "src": "/favicon-192.png", "type": "image/png", "sizes": "192x192" }, 4 | { "src": "/favicon-512.png", "type": "image/png", "sizes": "512x512" } 5 | ] 6 | } -------------------------------------------------------------------------------- /src/pages/de/thema.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: '@layouts/TagArchive.astro' 3 | title: Alle Themen 4 | description: Alle themen auf dieser Webseite mit Links zu Posts. 5 | collection: blog 6 | reference: tagArchive 7 | --- 8 | -------------------------------------------------------------------------------- /src/pages/nl/onderwerp.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: '@layouts/TagArchive.astro' 3 | title: Alle onderwerpen 4 | description: Alle onderwerp met posts op deze website. 5 | collection: blog 6 | reference: tagArchive 7 | --- 8 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface CommonFrontmatter { 2 | title?: string, 3 | reference?: string, 4 | description?: string, 5 | image?: string, 6 | previewImage?: string, 7 | canonicalURL?: string, 8 | file?: string, 9 | } 10 | -------------------------------------------------------------------------------- /src/pages/nl/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Thuis 3 | layout: '@layouts/Page.astro' 4 | reference: home 5 | description: 6 | --- 7 | Welkom! 8 | 9 | Dit is een demo-site voor de [Astro i18n blog starter](https://github.com/kslstn/astro-i18n-blog-starter). -------------------------------------------------------------------------------- /src/pages/de/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Home 3 | layout: '@layouts/Page.astro' 4 | reference: home 5 | --- 6 | Herzlich Willkommen! 7 | 8 | Dies ist eine Demo-Webseite für den [Astro i18n blog starter](https://github.com/kslstn/astro-i18n-blog-starter). 9 | -------------------------------------------------------------------------------- /src/components/PeopleList.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { people, getPersonPath } from "@utilities/people"; 3 | --- 4 | 5 | -------------------------------------------------------------------------------- /src/pages/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Home 3 | layout: '@layouts/Page.astro' 4 | reference: home 5 | description: This is the English homepage. 6 | --- 7 | Welcome! 8 | 9 | This is a demo site for the [Astro i18n blog starter](https://github.com/kslstn/astro-i18n-blog-starter). -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | -------------------------------------------------------------------------------- /src/components/FormattedDate.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | date: Date; 4 | } 5 | const { date } = Astro.props; 6 | const lang = Astro.currentLocale; 7 | --- 8 | 17 | -------------------------------------------------------------------------------- /src/consts.ts: -------------------------------------------------------------------------------- 1 | // Place any global data in this file. 2 | // You can import this data from anywhere in your site by using the `import` keyword. 3 | 4 | export const site = "https://example.com"; 5 | export const siteTitle = "Astro Blog Starter"; 6 | export const siteThemeColor = "#27AE60"; 7 | export const pagination = { 8 | postsPerPage: 12, 9 | }; 10 | -------------------------------------------------------------------------------- /public/favicon-dev.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "compilerOptions": { 4 | "strictNullChecks": false, 5 | "baseUrl": ".", 6 | "paths": { 7 | "@i18n/*": ["src/i18n/*"], 8 | "@components/*": ["src/components/*"], 9 | "@content/*": ["src/content/*"], 10 | "@layouts/*": ["src/layouts/*"], 11 | "@src/*": ["src/*"], 12 | "@utilities/*": ["src/utilities/*"], 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/utilities/getPageNumbers.ts: -------------------------------------------------------------------------------- 1 | import { pagination } from "../consts"; 2 | 3 | const getPageNumbers = (numberOfPosts: number) => { 4 | const numberOfPages = numberOfPosts / Number(pagination.postsPerPage); 5 | 6 | let pageNumbers: number[] = []; 7 | for (let i = 1; i <= Math.ceil(numberOfPages); i++) { 8 | pageNumbers = [...pageNumbers, i]; 9 | } 10 | 11 | return pageNumbers; 12 | }; 13 | 14 | export default getPageNumbers; 15 | -------------------------------------------------------------------------------- /src/styles/prose.css: -------------------------------------------------------------------------------- 1 | :where(.prose) { 2 | :where(h1, h2, h3, h4, p, figure, blockquote, dl, dd, ul, ol) { 3 | margin-top: 1em; 4 | } 5 | 6 | :where(ul, ol) { 7 | padding-inline-start: 1em; 8 | 9 | :where(ul, ol) { 10 | margin-top: 0; 11 | } 12 | } 13 | 14 | :where(ul) { 15 | list-style-type: initial; 16 | } 17 | 18 | :where(ol) { 19 | list-style-type: decimal; 20 | } 21 | 22 | :where(li) :where(p:first-child) { 23 | margin: 0; 24 | } 25 | } -------------------------------------------------------------------------------- /src/layouts/Page.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import MainI18n from "@components/MainI18n.astro"; 3 | import type { CommonFrontmatter } from "@src/types"; 4 | interface Props { 5 | frontmatter: CommonFrontmatter 6 | } 7 | 8 | const 9 | {frontmatter} = Astro.props, 10 | title = frontmatter?.title || ''; 11 | --- 12 | 13 |
14 | {title && 15 |

{title}

16 | } 17 | 18 |
19 |
20 | -------------------------------------------------------------------------------- /src/layouts/404.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Html from "@components/Html.astro"; 3 | import Header from "@components/Header.astro"; 4 | import type { CommonFrontmatter } from "@src/types"; 5 | 6 | interface Props { 7 | frontmatter: CommonFrontmatter 8 | } 9 | 10 | const 11 | {frontmatter} = Astro.props, 12 | title = frontmatter.title; 13 | --- 14 | 15 |
16 |
17 |
18 |

{title}

19 | 20 |
21 |
22 | -------------------------------------------------------------------------------- /src/components/TranslationsBanner.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const currentLocale = Astro.currentLocale 3 | --- 4 | 7 | 8 | 13 | 14 | -------------------------------------------------------------------------------- /src/utilities/getPagePath.ts: -------------------------------------------------------------------------------- 1 | import { defaultLocale } from '@i18n/i18n'; 2 | import slugify from '@sindresorhus/slugify'; 3 | 4 | export default function getPagePath(locale: string, directories:Array, slug: string, addLeadingSlash: boolean = true){ 5 | const leadingSlash = addLeadingSlash ? '/' : '' 6 | const localePath = locale === defaultLocale ? '' : `${locale}/` 7 | let directoryPath = '' 8 | directories.map((dir)=> directoryPath += `${ slugify(dir)}/`) 9 | slug = `${slugify(slug)}` 10 | return `${leadingSlash}${localePath}${directoryPath}${slug}` 11 | } -------------------------------------------------------------------------------- /src/components/PostTags.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getTagPath } from '@utilities/tags'; 3 | import {uiStrings} from "@i18n/uiStrings"; 4 | interface Props { 5 | tags: string[] 6 | } 7 | const {tags} = Astro.props 8 | const uiString = tags.length === 1 ? uiStrings.tagHeadingSingular[Astro.currentLocale] : uiStrings.tagHeadingPlural[Astro.currentLocale] 9 | --- 10 | 11 |

12 | {uiString}: 13 | {tags.map((tag, index) => ( 14 | {tag} 15 | {index < tags.length - 1 ? 16 | ', ' : '' 17 | } 18 | ))}. 19 |

-------------------------------------------------------------------------------- /src/components/HeaderLink.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { HTMLAttributes } from 'astro/types'; 3 | type Props = HTMLAttributes<'a'>; 4 | 5 | const { href, class: className, ...props } = Astro.props; 6 | const { pathname } = Astro.url; 7 | const ariaCurrent = href === pathname || href === pathname.replace(/\/$/, '') ? 'page' : '' ; 8 | --- 9 |
  • 10 | 16 | 17 | 18 |
  • 19 | 20 | 25 | -------------------------------------------------------------------------------- /src/components/DescriptionMeta.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import {truncateDescription, getPageDescription} from '@utilities/getPageDescription' 3 | interface Props { 4 | description: string, 5 | file: string 6 | } 7 | 8 | let {description, file} = Astro.props 9 | if (!description){ 10 | const page = (await Astro.glob('../**/*.{md,mdx}')).find(page => page.file === file) 11 | description = getPageDescription(page) 12 | } 13 | else description = truncateDescription(description) 14 | if (!description) console.warn("⚠️ This page doesn't have a description.") 15 | --- 16 | {description && 17 | 18 | } -------------------------------------------------------------------------------- /src/utilities/getPostsToRenderInRSS.ts: -------------------------------------------------------------------------------- 1 | import { getRelativePostPath } from '@utilities/getPostPath'; 2 | import getPublishedPosts from '@utilities/getPublishedPosts'; 3 | import {getLocaleFromUrl} from '@i18n/utilities'; 4 | import type { APIContext } from 'astro'; 5 | 6 | export async function getPostsToRenderInRSS(_context: APIContext, locale: string, collection: "blog") { 7 | const posts = (await getPublishedPosts(locale, collection)) 8 | .slice(0, 50) 9 | .map((post) => ({ 10 | ...post.data, 11 | link: getRelativePostPath(locale || getLocaleFromUrl(post.slug), collection, post.slug), 12 | body: post.body 13 | })); 14 | return posts 15 | } -------------------------------------------------------------------------------- /src/header.ts: -------------------------------------------------------------------------------- 1 | export type navigationItem = Readonly<{ 2 | path: string, 3 | label: string, 4 | }> 5 | 6 | export const headerMenu: Record> = { 7 | en: { 8 | items: [{ 9 | path: '/', 10 | label: 'Home', 11 | }, 12 | { 13 | path: '/blog', 14 | label: 'Blog' 15 | }, 16 | { 17 | path: '/tag', 18 | label: 'Tags' 19 | }, 20 | { 21 | path: '/about', 22 | label: 'About' 23 | }] 24 | }, 25 | de: { 26 | items: [{ 27 | path: '/de/', 28 | label: 'Home' 29 | }, 30 | { 31 | path: '/de/blog', 32 | label: 'Blog' 33 | }, 34 | { 35 | path: '/de/thema', 36 | label: 'Themen' 37 | },] 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /src/components/TranslationLinks.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { uiStrings } from '@i18n/uiStrings'; 3 | import type { TranslationLink } from './MainI18n.astro'; 4 | 5 | interface Props { 6 | translationLinks: TranslationLink[] 7 | } 8 | 9 | const 10 | { translationLinks } = Astro.props, 11 | pageTranslationsAvailableIn = `${uiStrings.pageTranslationsAvailableIn[ Astro.currentLocale]} `; 12 | --- 13 | { pageTranslationsAvailableIn && 14 |

    15 | {pageTranslationsAvailableIn} 16 | {translationLinks.map((translation, index) => ( 17 | 18 | {translation.label} 19 | <>{index < translationLinks.length - 1 ? ', ' : null}))}. 20 |

    21 | } -------------------------------------------------------------------------------- /src/utilities/getPublishedPosts.ts: -------------------------------------------------------------------------------- 1 | import { type CollectionEntry, getCollection } from 'astro:content'; 2 | import {getLocaleFromUrl} from '@i18n/utilities' 3 | 4 | export default async function getPublishedPosts(locale: string, collection: "blog" = 'blog', sort: string = 'reverseChronological'){ 5 | let posts = (await getCollection(collection)).filter(({ data }) => !data.secret) 6 | if (locale !== '') posts = posts.filter(function(entry:CollectionEntry<'blog'>){ return getLocaleFromUrl(entry.slug) === locale }) 7 | if (sort === 'reverseChronological') posts = posts.sort((a:CollectionEntry<'blog'>, b:CollectionEntry<'blog'>) => 8 | b.data.pubDate.valueOf() - a.data.pubDate.valueOf() 9 | ) 10 | return posts 11 | }; -------------------------------------------------------------------------------- /src/content/config.ts: -------------------------------------------------------------------------------- 1 | import { defineCollection, z } from 'astro:content'; 2 | 3 | const blogCollection = defineCollection({ 4 | type: 'content', 5 | // Type-check frontmatter using a schema 6 | schema: z.object({ 7 | title: z.string(), 8 | description: z.string().optional(), 9 | author: z.string().optional(), 10 | reference: z.string().optional(), 11 | // Transform string to Date object 12 | pubDate: z.coerce.date(), 13 | updatedDate: z.coerce.date().optional(), 14 | previewImage: z.string().optional(), 15 | secret: z.boolean().default(false), 16 | tags: z.array(z.string()).default(['other']), 17 | canonicalURL: z.string().optional(), 18 | }), 19 | }); 20 | 21 | export const collections = { 22 | 'blog' : blogCollection, 23 | }; -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/i18n/utilities.ts: -------------------------------------------------------------------------------- 1 | import { defaultLocale, locales } from '@i18n/i18n'; 2 | 3 | export function getLocaleFromUrl(url: string): string { 4 | if (url === undefined) return undefined 5 | const parts = url.split('/').filter(function (el) {return el !== ''}) 6 | let match = '' 7 | parts.forEach((part)=>{ 8 | if (locales.includes(part)) match = part 9 | }) 10 | if (match) return match 11 | return defaultLocale; 12 | } 13 | 14 | export function localeIsInUrl(locale: string, url: string): boolean{ 15 | return url.split('/').filter(function (el) {return el !== ''}).includes(locale) 16 | } 17 | 18 | export async function getStaticPaths(pages: any[]): Promise { 19 | return pages.map(page => ({ 20 | params: { slug: page.slug }, 21 | props: { page }, 22 | })); 23 | } -------------------------------------------------------------------------------- /src/layouts/TagArchive.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getUniqueTags } from '@utilities/tags'; 3 | import { getTagPath } from '@utilities/tags'; 4 | import Page from '@layouts/Page.astro' 5 | import type { CommonFrontmatter } from '@src/types'; 6 | 7 | interface Frontmatter extends CommonFrontmatter{ 8 | collection: string 9 | } 10 | 11 | interface Props{ 12 | frontmatter: Frontmatter 13 | } 14 | 15 | const {frontmatter} = Astro.props 16 | const tags = (await getUniqueTags(frontmatter.collection, Astro.currentLocale)).filter((tag) => { 17 | return tag.slugified !== 'other' 18 | }) 19 | --- 20 | 21 | 22 |
      23 | {tags.map(({ slugified, name }) => 24 |
    • 25 | {name} 26 |
    • 27 | )} 28 |
    29 |
    -------------------------------------------------------------------------------- /src/layouts/PostsPerTag.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Page from '@layouts/Page.astro' 3 | import { getPostsByTag } from '@utilities/tags'; 4 | import { getRelativePostPath } from '@utilities/getPostPath'; 5 | import type { CommonFrontmatter } from '@src/types'; 6 | 7 | interface Frontmatter extends CommonFrontmatter{ 8 | tag?: string, 9 | collection?: string, 10 | } 11 | interface Props{ 12 | frontmatter: Frontmatter, 13 | } 14 | 15 | const {frontmatter } = Astro.props; 16 | const filteredPosts = await getPostsByTag('blog', frontmatter.tag, Astro.currentLocale); 17 | --- 18 | 19 | 20 |
      21 | {filteredPosts.map(({ data, slug }) => ( 22 |
    • 23 | {data.title} 24 |
    • 25 | ))} 26 |
    27 |
    -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astro-blog", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "dev": "astro dev", 7 | "start": "astro dev", 8 | "build": "astro build", 9 | "preview": "astro preview", 10 | "astro": "astro" 11 | }, 12 | "dependencies": { 13 | "@astrojs/mdx": "^2.3.1", 14 | "@astrojs/rss": "^4.0.7", 15 | "@astrojs/sitemap": "^3.1.6", 16 | "@exampledev/new.css": "^1.1.3", 17 | "@sindresorhus/slugify": "^2.2.1", 18 | "astro": "^4.15.5", 19 | "markdown-it": "^14.1.0", 20 | "rehype-external-links": "^3.0.0", 21 | "remove-markdown": "^0.5.5", 22 | "sanitize-html": "^2.13.0" 23 | }, 24 | "devDependencies": { 25 | "@types/markdown-it": "^13.0.9", 26 | "@types/remove-markdown": "^0.3.4", 27 | "@types/sanitize-html": "^2.13.0" 28 | } 29 | } -------------------------------------------------------------------------------- /src/utilities/getPostPath.ts: -------------------------------------------------------------------------------- 1 | import getPagePath from '@utilities/getPagePath' 2 | import { collectionDirectoryNames } from '@i18n/i18n'; 3 | import { locales } from '@i18n/i18n'; 4 | import { site } from '@src/consts' 5 | 6 | export function getRelativePostPath(locale: string, collection: string, slug: string, addLeadingSlash: boolean = true): string{ 7 | const 8 | trueSlug = slug.slice(slug.indexOf('/') + 1), // remove /[locale]/ from start of slug 9 | collectionDirectory = [collectionDirectoryNames[collection][locale]]; 10 | if (locales.includes(locale)) return getPagePath(locale, collectionDirectory, trueSlug, addLeadingSlash) 11 | else throw new Error(`Unknown locale: ${locale}`) 12 | } 13 | 14 | export function getAbsolutePostPath(locale: string, collection: string, slug: string): string{ 15 | return `${site}${getRelativePostPath(locale, collection, slug, true)}` 16 | } 17 | -------------------------------------------------------------------------------- /src/pages/rss-de.xml.ts: -------------------------------------------------------------------------------- 1 | import rss from '@astrojs/rss'; 2 | import { siteTitle } from '../consts'; 3 | import { uiStrings } from '@i18n/uiStrings'; 4 | import { getPostsToRenderInRSS } from '@utilities/getPostsToRenderInRSS'; 5 | import sanitizeHtml from 'sanitize-html'; 6 | import MarkdownIt from 'markdown-it'; 7 | import type { APIContext } from 'astro'; 8 | const parser = new MarkdownIt(); 9 | 10 | export async function GET(context: APIContext) { 11 | const postsToRender = await getPostsToRenderInRSS(context, 'de', 'blog') 12 | 13 | return rss({ 14 | title: siteTitle, 15 | description: uiStrings.siteDescription.de, 16 | site: context.site, 17 | items: postsToRender.map((post) => ({ 18 | title: post.title ?? "", 19 | pubDate: post.pubDate ?? new Date(), 20 | link: post.link, 21 | content: sanitizeHtml(parser.render(post.body)), 22 | ...post 23 | })), 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /src/pages/rss-en.xml.ts: -------------------------------------------------------------------------------- 1 | import rss from '@astrojs/rss'; 2 | import { siteTitle } from '../consts'; 3 | import { uiStrings } from '@i18n/uiStrings'; 4 | import { getPostsToRenderInRSS } from '@utilities/getPostsToRenderInRSS'; 5 | import sanitizeHtml from 'sanitize-html'; 6 | import MarkdownIt from 'markdown-it'; 7 | import type { APIContext } from 'astro'; 8 | const parser = new MarkdownIt(); 9 | 10 | export async function GET(context: APIContext) { 11 | const postsToRender = await getPostsToRenderInRSS(context, 'en', 'blog') 12 | 13 | return rss({ 14 | title: siteTitle, 15 | description: uiStrings.siteDescription.en, 16 | site: context.site, 17 | items: postsToRender.map((post) => ({ 18 | title: post.title ?? "", 19 | pubDate: post.pubDate ?? new Date(), 20 | link: post.link, 21 | content: sanitizeHtml(parser.render(post.body)), 22 | ...post 23 | })), 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /src/pages/rss-nl.xml.ts: -------------------------------------------------------------------------------- 1 | import rss from '@astrojs/rss'; 2 | import { siteTitle } from '../consts'; 3 | import { uiStrings } from '@i18n/uiStrings'; 4 | import { getPostsToRenderInRSS } from '@utilities/getPostsToRenderInRSS'; 5 | import sanitizeHtml from 'sanitize-html'; 6 | import MarkdownIt from 'markdown-it'; 7 | import type { APIContext } from 'astro'; 8 | const parser = new MarkdownIt(); 9 | 10 | export async function GET(context: APIContext) { 11 | const postsToRender = await getPostsToRenderInRSS(context, 'nl', 'blog') 12 | 13 | return rss({ 14 | title: siteTitle, 15 | description: uiStrings.siteDescription.nl, 16 | site: context.site, 17 | items: postsToRender.map((post) => ({ 18 | title: post.title ?? "", 19 | pubDate: post.pubDate ?? new Date(), 20 | link: post.link, 21 | content: sanitizeHtml(parser.render(post.body)), 22 | ...post 23 | })), 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /src/pages/nl/over.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | layout: '@layouts/Page.astro' 3 | title: "Over deze website" 4 | description: "Lorem ipsum dolor sit amet" 5 | reference: about 6 | --- 7 | import PeopleList from '@components/PeopleList.astro' 8 | 9 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut 10 | labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo 11 | viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam 12 | adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus 13 | et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus 14 | vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque 15 | sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet. 16 | 17 | Dit is ons team: 18 | 19 | -------------------------------------------------------------------------------- /src/layouts/Person.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Page from '@layouts/Page.astro' 3 | import type { Person } from "@utilities/people" 4 | import type { CommonFrontmatter } from '@src/types' 5 | export interface PersonFrontmatter extends CommonFrontmatter{ 6 | person: Person 7 | } 8 | interface Props { 9 | frontmatter: PersonFrontmatter 10 | } 11 | const { frontmatter } = Astro.props 12 | const person = frontmatter.person 13 | const contactDataAreAvailable = person.mail || person.mastodon 14 | --- 15 | 16 | 17 | {contactDataAreAvailable && 18 | 19 | 20 | {person.mail && 21 | 22 | } 23 | {person.mastodon && 24 | 25 | } 26 | 27 |
    Mail{person.mail}
    Mastodon{person.mastodon}
    28 | } 29 | 30 |

    {person.description[Astro.currentLocale]}

    31 |
    -------------------------------------------------------------------------------- /src/pages/rss.xml.ts: -------------------------------------------------------------------------------- 1 | import rss from '@astrojs/rss'; 2 | import { siteTitle } from '../consts'; 3 | import { uiStrings } from '@i18n/uiStrings'; 4 | import { defaultLocale } from '@i18n/i18n'; 5 | import { getPostsToRenderInRSS } from '@utilities/getPostsToRenderInRSS'; 6 | import sanitizeHtml from 'sanitize-html'; 7 | import MarkdownIt from 'markdown-it'; 8 | import type { APIContext } from 'astro'; 9 | const parser = new MarkdownIt(); 10 | 11 | export async function GET(context: APIContext){ 12 | const postsToRender = await getPostsToRenderInRSS(context, '', 'blog') 13 | 14 | return rss({ 15 | title: siteTitle, 16 | description: uiStrings.siteDescription[defaultLocale], 17 | site: context.site, 18 | items: postsToRender.map((post) => ({ 19 | title: post.title ?? "", 20 | pubDate: post.pubDate ?? new Date(), 21 | link: post.link, 22 | content: sanitizeHtml(parser.render(post.body)), 23 | ...post 24 | })), 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /src/i18n/i18n.ts: -------------------------------------------------------------------------------- 1 | export type Locale = "en" | "nl" | "de" | string; 2 | 3 | interface Fallback { 4 | [key: string]: string 5 | } 6 | type PathNames = { 7 | [key: string]: { 8 | [locale in Locale]: string 9 | } 10 | }; 11 | 12 | export const defaultLocale: string = "en" 13 | export const locales = ["en", "nl", "de"] 14 | export const fallback: Fallback = { 15 | nl: "en", 16 | } 17 | // Define the paths for collections 18 | export const collectionDirectoryNames: PathNames = { 19 | blog: { 20 | en: 'blog', 21 | de: 'blog', 22 | nl: 'blog' 23 | }, 24 | } 25 | export const directoryNames: PathNames = { 26 | // Define the path for the tag pages (tags list, posts per tag). 27 | tags: { 28 | en: 'tag', 29 | de: 'thema', 30 | nl: 'onderwerp' 31 | }, 32 | // Define the path for people's profile pages. Should arguably be the same as the locale's About page's slug. 33 | people: { 34 | en: 'about', 35 | de: 'ueber', 36 | nl: 'over' 37 | } 38 | } -------------------------------------------------------------------------------- /src/components/Byline.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { Person } from '@utilities/people' 3 | import FormattedDate from '@components/FormattedDate.astro' 4 | import { people, getPersonPath } from '@utilities/people' 5 | interface Props { 6 | authorFullName: string, 7 | pubDate: Date, 8 | updatedDate: Date 9 | } 10 | const { authorFullName = '', pubDate, updatedDate } = Astro.props 11 | let person: Person = people.find(person => {return person.fullName === authorFullName}) 12 | const hasAuthorLink: boolean = typeof person === 'object' 13 | --- 14 |

    15 | {authorFullName && 16 | { hasAuthorLink && {person.fullName} } 17 | { !hasAuthorLink && {authorFullName} } 18 | , 19 | } 20 | 21 | {updatedDate && ( 22 | 23 | Last updated on 24 | 25 | ) 26 | } 27 |

    -------------------------------------------------------------------------------- /src/pages/about.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | layout: '@layouts/Page.astro' 3 | title: "About this website" 4 | description: 'About our team' 5 | reference: about 6 | --- 7 | import PeopleList from '@components/PeopleList.astro' 8 | 9 | Morbi **tristique** senectus et netus. Id semper risus in hendrerit gravida rutrum quisque non 10 | tellus. Habitasse platea dictumst quisque sagittis purus sit amet. Tellus molestie nunc non 11 | blandit massa. Cursus vitae congue mauris rhoncus. Accumsan tortor posuere ac ut. Fringilla urna 12 | porttitor rhoncus dolor. Elit ullamcorper dignissim cras tincidunt lobortis. In cursus turpis 13 | massa tincidunt dui ut ornare lectus. Integer feugiat scelerisque varius morbi enim nunc. 14 | Bibendum neque egestas congue quisque egestas diam. Cras ornare arcu dui vivamus arcu felis 15 | bibendum. Dignissim suspendisse in est ante in nibh mauris. Sed tempus urna et pharetra pharetra 16 | massa massa ultricies mi. 17 | 18 | Here's our team: 19 | 20 | -------------------------------------------------------------------------------- /astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'astro/config'; 2 | import mdx from '@astrojs/mdx'; 3 | import rehypeExternalLinks from 'rehype-external-links' 4 | import { defaultLocale, locales } from './src/i18n/i18n'; 5 | import { site } from './src/consts'; 6 | 7 | const sitemapLocales = Object.fromEntries(locales.map((_, i) => [locales[i], locales[i]])) // Create an object with keys and values based on locales 8 | 9 | import sitemap from '@astrojs/sitemap'; 10 | 11 | // https://astro.build/config 12 | export default defineConfig({ 13 | site: site, 14 | integrations: [ 15 | mdx(), 16 | sitemap({ 17 | filter: (page) => page.secret !== true, 18 | i18n: { 19 | defaultLocale: defaultLocale, 20 | locales: sitemapLocales, 21 | } 22 | }) 23 | ], 24 | i18n: { 25 | defaultLocale: defaultLocale, 26 | locales: locales, 27 | }, 28 | markdown: { 29 | rehypePlugins:[[ 30 | rehypeExternalLinks, { 31 | target: '_blank', 32 | rel: ['nofollow', 'noreferrer'], 33 | } 34 | ]] 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /src/utilities/getPagination.ts: -------------------------------------------------------------------------------- 1 | import { pagination } from '../consts' 2 | import getPageNumbers from "@utilities/getPageNumbers"; 3 | 4 | interface GetPaginationProps { 5 | posts: T; 6 | page: string | number; 7 | isIndex?: boolean; 8 | } 9 | 10 | const postsPerPage = pagination.postsPerPage 11 | 12 | const getPagination = ({ 13 | posts, 14 | page, 15 | isIndex = false, 16 | }: GetPaginationProps) => { 17 | const totalPagesArray = getPageNumbers(posts.length); 18 | const totalPages = totalPagesArray.length; 19 | 20 | const currentPage = isIndex 21 | ? 1 22 | : page && !isNaN(Number(page)) && totalPagesArray.includes(Number(page)) 23 | ? Number(page) 24 | : 0; 25 | 26 | const lastPost = isIndex ? postsPerPage : currentPage * postsPerPage; 27 | const startPost = isIndex ? 0 : lastPost - postsPerPage; 28 | const paginatedPosts = posts.slice(startPost, lastPost); 29 | 30 | return { 31 | totalPages, 32 | currentPage, 33 | paginatedPosts, 34 | }; 35 | }; 36 | 37 | export default getPagination; -------------------------------------------------------------------------------- /src/components/Header.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { defaultLocale, fallback } from "@i18n/i18n"; 3 | import HeaderLink from "./HeaderLink.astro"; 4 | import { headerMenu } from "@src/header"; 5 | import type { navigationItem } from "@src/header"; 6 | 7 | // If no items are available for the page's locale, the items for the defaultLocale will be used: 8 | const menuLocale: string = headerMenu[Astro.currentLocale] 9 | ? Astro.currentLocale 10 | : fallback[Astro.currentLocale] || defaultLocale; 11 | const headerMenuItems: navigationItem[] = headerMenu[menuLocale].items; 12 | --- 13 | 14 |
    15 | 25 |
    26 | 27 | 37 | -------------------------------------------------------------------------------- /src/components/SkipLink.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { uiStrings } from '@i18n/uiStrings'; 3 | --- 4 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /src/layouts/Post.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Byline from "@components/Byline.astro"; 3 | import PostTags from "@components/PostTags.astro"; 4 | import MainI18n from "@components/MainI18n.astro"; 5 | import type { CollectionEntry } from "astro:content"; 6 | 7 | type Props = CollectionEntry<"blog">["data"]; 8 | 9 | const 10 | frontmatter = Astro.props, 11 | { title, pubDate, updatedDate, previewImage, tags, author } = Astro.props, 12 | realTags: string[] = tags.filter((tag)=>{return tag !== 'other'}); 13 | --- 14 | 15 | 16 |
    17 |
    18 |
    19 | {previewImage && } 20 |

    {title}

    21 | 22 |
    23 |
    24 | 25 |
    26 | {realTags.length > 0 && 27 |
    28 | 29 |
    30 | } 31 |
    32 |
    33 |
    -------------------------------------------------------------------------------- /src/components/Html.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Head from '@components/Head.astro'; 3 | import SkipLink from '@components/SkipLink.astro'; 4 | import type { CommonFrontmatter } from '@src/types'; 5 | interface Props{ 6 | frontmatter: CommonFrontmatter, 7 | includeCanonicalMeta?: boolean, 8 | includeSkipLink?: boolean 9 | } 10 | 11 | const {frontmatter, includeCanonicalMeta = true, includeSkipLink = true} = Astro.props 12 | const mode: string = import.meta.env.MODE 13 | const lang = Astro.currentLocale 14 | --- 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {includeSkipLink && 24 | 25 | } 26 | 27 | 28 | {mode === 'development' && 29 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/utilities/people.ts: -------------------------------------------------------------------------------- 1 | import type { PersonData } from '@src/people' 2 | import slugify from '@sindresorhus/slugify'; 3 | import getPagePath from '@src/utilities/getPagePath'; 4 | import { directoryNames } from '@i18n/i18n'; 5 | import { peopleData } from '@src/people'; 6 | 7 | export interface Person extends PersonData { 8 | fullName: string, 9 | id: string 10 | } 11 | 12 | const getFullName = function(person: PersonData){ 13 | let fullName = person.givenName 14 | if (person.surName) fullName += ` ${person.surName}` 15 | if (!fullName) throw new Error('Person needs at least a given name or surname.') 16 | fullName = fullName.trim() 17 | return fullName 18 | } 19 | 20 | export const people = peopleData 21 | .filter(person =>{ return person.publishProfile !== false}) 22 | .map(person => ({ ...person, fullName: getFullName(person) })) 23 | .map(person => ({ ...person, id: slugify(person.fullName) })) 24 | 25 | export const getPersonPath = (locale: string, id: string, addLeadingSlash: boolean = true) => { 26 | const directories = [directoryNames.people[locale]] 27 | return getPagePath(locale, directories, id, addLeadingSlash) 28 | } -------------------------------------------------------------------------------- /src/pages/[...blogPost].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { type CollectionEntry } from 'astro:content'; 3 | import { locales } from '@i18n/i18n'; 4 | import { getLocaleFromUrl} from '@i18n/utilities'; 5 | import Post from '@layouts/Post.astro'; 6 | import { getRelativePostPath } from '@utilities/getPostPath'; 7 | import getPublishedPosts from '@utilities/getPublishedPosts'; 8 | import { getPostDescription } from '@utilities/getPageDescription'; 9 | 10 | export async function getStaticPaths() { 11 | const collection = 'blog' 12 | const posts = await getPublishedPosts('', collection, ''); 13 | let postsToRender = [] 14 | 15 | for (let locale of locales) { 16 | const localePosts = posts 17 | .filter(function(entry){ return getLocaleFromUrl(entry.slug) === locale }) 18 | .map((post) => ({ 19 | params: { blogPost: `${getRelativePostPath(locale, collection, post.slug, false)}` }, 20 | props: post, 21 | })); 22 | postsToRender.push(...localePosts) 23 | } 24 | return postsToRender 25 | } 26 | type Props = CollectionEntry<'blog'>; 27 | 28 | const post = Astro.props; 29 | const { Content } = await post.render(); 30 | post.data.description = getPostDescription(post) 31 | --- 32 | 33 | 34 | 35 | 16 |
    17 |
    18 |

    {title}

    19 | 20 |
    21 | 37 |
    38 | -------------------------------------------------------------------------------- /src/utilities/getPageDescription.ts: -------------------------------------------------------------------------------- 1 | import removeMd from 'remove-markdown' 2 | 3 | const getFirstSentence = function(s: string): string{ 4 | const regexp = /(^.*?[a-z]{2,}[.!?]+)\s+\W*[A-Z]/gm 5 | const matches = s.matchAll(regexp); 6 | let result = '' 7 | for (const match of matches) { 8 | result = match[1] 9 | break 10 | } 11 | return result 12 | } 13 | 14 | export function truncateDescription(description: string): string{ 15 | if (!description) return '' 16 | const maxNumberOfWords = 22 17 | if (description.split(" ").length > maxNumberOfWords){ 18 | description = `${description.split(" ").splice(0,maxNumberOfWords).join(" ")} …` 19 | } 20 | if (description.length > 150) description = `${description.substring(0, 150)} …` 21 | return description 22 | } 23 | 24 | export function getPageDescription(page: any): string{ 25 | // Unlike for getPostDescription(), here we don't attempt to get the first sentence. The body may be to short for it. 26 | if (page) return (typeof page.rawContent() === 'undefined') ? '' : truncateDescription(removeMd(page.rawContent())) 27 | return '' 28 | } 29 | 30 | export function getPostDescription(post: any): string{ 31 | if (post) return post.data.description ? truncateDescription(post.data.description) : truncateDescription(getFirstSentence(removeMd(post.body))) 32 | return '' 33 | } 34 | -------------------------------------------------------------------------------- /src/pages/[...person].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { locales } from '@i18n/i18n'; 3 | import { uiStrings } from '@i18n/uiStrings'; 4 | import { people, getPersonPath } from "@utilities/people"; 5 | import Person from "@layouts/Person.astro"; 6 | import { truncateDescription } from "@utilities/getPageDescription"; 7 | import type { Person as PersonType } from "@utilities/people" 8 | import type { Locale } from '@i18n/i18n'; 9 | 10 | export async function getStaticPaths() { 11 | const getDescription = function(locale: Locale, person: PersonType){ 12 | const fullDescription = person.description[locale] 13 | return truncateDescription(fullDescription ? fullDescription : `${uiStrings.personProfilePageDescription[locale]} ${person.fullName}.`) 14 | } 15 | let allPeoplePages:any = [] 16 | for (let locale of locales) { 17 | const peopleParsed = people 18 | .map(person=>( 19 | { 20 | params: { 21 | person: `${getPersonPath(locale, person.id, false)}`, 22 | }, 23 | props: { 24 | title: person.fullName, 25 | description: getDescription(locale, person), 26 | person: person, 27 | } 28 | } 29 | )) 30 | allPeoplePages.push(...peopleParsed) 31 | } 32 | return allPeoplePages 33 | } 34 | const frontmatter = Astro.props 35 | --- 36 | -------------------------------------------------------------------------------- /src/utilities/tags.ts: -------------------------------------------------------------------------------- 1 | import getPagePath from '@src/utilities/getPagePath'; 2 | import getPublishedPosts from '@utilities/getPublishedPosts'; 3 | import slugify from '@sindresorhus/slugify'; 4 | import { directoryNames } from '@i18n/i18n'; 5 | 6 | export type Tags = { 7 | slugified: string; 8 | name: string; 9 | }[] 10 | 11 | export async function getUniqueTags(collection:any, locale: string = ''): Promise{ 12 | return (await getPublishedPosts(locale, collection, '')) 13 | .flatMap(post => post.data.tags) 14 | .map(tag => ({ 15 | slugified: slugify(tag), 16 | name: tag 17 | })) 18 | .filter((value, index, self) => 19 | // Tag is slugified here just in case in other places we'd use a different method to slugify it. 20 | self.findIndex(tag => tag.slugified === value.slugified) === index 21 | ) 22 | .sort((tagA, tagB) => tagA.slugified.localeCompare(tagB.slugified)); 23 | }; 24 | 25 | export async function getPostsByTag(collection: "blog", tag: string, locale: string) { 26 | return (await getPublishedPosts(locale, collection)).filter(post => post.data.tags.map(item => {return slugify(item)}).includes(slugify(tag))) 27 | } 28 | 29 | export const getTagPath = (locale: string, tag: string, addLeadingSlash: boolean = true): string=>{ 30 | const directories = [directoryNames.tags[locale]] 31 | return getPagePath(locale, directories, tag, addLeadingSlash) 32 | } -------------------------------------------------------------------------------- /src/pages/[...blogPostsPerTag].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getUniqueTags, getTagPath} from '@utilities/tags'; 3 | import { locales } from '@i18n/i18n'; 4 | import { uiStrings } from '@i18n/uiStrings'; 5 | import PostsPerTag from "@layouts/PostsPerTag.astro"; 6 | import { truncateDescription } from "@utilities/getPageDescription"; 7 | import type { Tags } from '@utilities/tags'; 8 | 9 | export async function getStaticPaths() { 10 | const collection = 'blog' 11 | let allPostsPerTagPages: any[] = [] 12 | const getDescription = function(locale: string, tagName:string): string{ 13 | return truncateDescription(`${uiStrings.postsPerTagPageDescription[locale]} ${tagName}.`) 14 | } 15 | for (let locale of locales) { 16 | const realTags: Tags = (await getUniqueTags(collection, locale)) 17 | .filter((tag)=>{return tag.slugified !== 'other'}) 18 | const posts = realTags.map(tag=>( 19 | { 20 | params: { 21 | blogPostsPerTag: `${getTagPath(locale, tag.slugified, false)}` 22 | }, 23 | props: { 24 | title: `${uiStrings.tagHeadingSingular[locale]}: ${tag.name}`, 25 | description: getDescription(locale, tag.name), 26 | tag: tag.slugified, 27 | collection: collection 28 | } 29 | } 30 | )) 31 | allPostsPerTagPages.push(...posts) 32 | } 33 | return allPostsPerTagPages 34 | } 35 | --- 36 | -------------------------------------------------------------------------------- /src/styles/global.css: -------------------------------------------------------------------------------- 1 | /* Reset based on ideas from https: //chriscoyier.net/2023/10/03/being-picky-about-a-css-reset-for-fun-pleasure/ */ 2 | *{ 3 | box-sizing: border-box; 4 | } 5 | 6 | *::before, 7 | *::after { 8 | box-sizing: inherit; 9 | } 10 | 11 | /* This makes it so iPhones don’t dork up the font size in landscape mode */ 12 | html { 13 | -moz-text-size-adjust: none; 14 | -webkit-text-size-adjust: none; 15 | text-size-adjust: none; 16 | } 17 | body{ 18 | min-height: 100svh; 19 | } 20 | body, 21 | :where(h1,h2, h3, h4, p, figure, blockquote, dl, dd){ 22 | margin: 0; 23 | } 24 | :where(ul, ol){ 25 | list-style-type: ''; 26 | margin: 0; 27 | padding: 0 28 | } 29 | :where(input, button, textarea, select) { 30 | font: inherit; 31 | } 32 | :where(h1, h2, h3, h4) { 33 | text-wrap: balance; 34 | } 35 | :where(p, figcaption, blockquote, dl, dd, li) { 36 | text-wrap: pretty; 37 | } 38 | :where(img){ 39 | max-width: 100%; 40 | height: auto; 41 | } 42 | :target { 43 | scroll-margin-block: 1em; 44 | } 45 | 46 | 47 | /* Tokens */ 48 | :root{ 49 | --space-xs: 0.5rem; 50 | --space-s: 1rem; 51 | --space-m: 2rem; 52 | 53 | --bg-light: white; 54 | 55 | --duration-link: 100ms; 56 | --duration-link-hover: 75ms; 57 | } 58 | 59 | @media (prefers-reduced-motion: reduce) { 60 | :root { 61 | --duration-link: 5ms; 62 | --duration-link-hover: 5ms; 63 | } 64 | } 65 | 66 | /* Basic layout */ 67 | 68 | body{ 69 | display: flex; 70 | flex-direction: column; 71 | } 72 | main{ 73 | flex-grow: 1; 74 | } 75 | -------------------------------------------------------------------------------- /src/i18n/uiStrings.ts: -------------------------------------------------------------------------------- 1 | import type { Locale } from "./i18n"; 2 | 3 | interface TypeUIStrings { 4 | [key: string]: { 5 | [locale in Locale] : string | undefined // If we'd enforce keys to be any of the already added language codes, it'd be impossible to add new locale strings before enabling that locale site-wide. 6 | } 7 | } 8 | 9 | export const uiStrings: TypeUIStrings = { 10 | siteDescription: { 11 | en: 'All the basics for a brand-new blog.', 12 | de: 'Die Basics für eine niegelnagelneues Blog', 13 | nl: 'De start voor een gloednieuw blog', 14 | }, 15 | skipLink: { 16 | en: 'Skip to content', 17 | de: 'Zum Inhalt springen', 18 | nl: 'Naar inhoud springen', 19 | }, 20 | pageTranslationsAvailableIn: { 21 | en: 'This page is also available in', 22 | de: 'Diese Seite gibt es auch auf', 23 | nl: 'Deze tekst is ook beschikbaar in het' 24 | }, 25 | en: { 26 | en: 'English', 27 | de: 'englisch', 28 | nl: 'Engels', 29 | }, 30 | de: { 31 | en: 'German', 32 | de: 'deutsch', 33 | nl: 'Duits', 34 | }, 35 | nl: { 36 | en: 'Dutch', 37 | de: 'niederländisch', 38 | nl: 'Nederlands' 39 | }, 40 | tagHeadingSingular: { 41 | en: 'Tag', 42 | de: 'Thema', 43 | nl: 'Onderwerp' 44 | }, 45 | tagHeadingPlural: { 46 | en: 'Tags', 47 | de: 'Themen', 48 | nl: 'Onderwerpen' 49 | }, 50 | postsPerTagPageDescription: { 51 | en: "Posts about", 52 | de: 'Posts zum Thema', 53 | nl: 'Posts met onderwerp' 54 | }, 55 | personProfilePageDescription: { 56 | en: "Profile of", 57 | de: 'Profil von', 58 | nl: 'Profiel van' 59 | }, 60 | pageNotFoundHeading: { 61 | en: 'Page not found', 62 | de: 'Seite nicht gefunden', 63 | nl: 'Pagina niet gevonden' 64 | }, 65 | pageNotFoundBody: { 66 | en: '404!', 67 | de: '404!', 68 | nl: '404!' 69 | }, 70 | }; 71 | -------------------------------------------------------------------------------- /src/pages/404.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Header from "@components/Header.astro"; 3 | import Html from "@components/Html.astro"; 4 | import { uiStrings } from "@i18n/uiStrings"; 5 | import { locales, defaultLocale } from "@i18n/i18n"; 6 | const title = uiStrings.pageNotFoundHeading[defaultLocale] 7 | --- 8 | 9 | 10 |
    11 |
    12 |
    13 | {locales.map((locale)=>( 14 |

    {uiStrings.pageNotFoundHeading[locale]}

    15 |

    {uiStrings.pageNotFoundBody[locale]}

    16 | <> 17 | {locale === defaultLocale && 18 | 22 | } 23 | 24 | ))} 25 | 26 |
    27 |
    28 | 29 | 30 | -------------------------------------------------------------------------------- /src/content/blog/nl/tweede-post.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: nl/mijn-tweede-post 3 | title: 'Tweede post' 4 | author: 'Ashley Devi' 5 | description: 'Ik ben makelaar' 6 | pubDate: 'Jul 15 2022' 7 | previewImage: '/blog-placeholder-3.jpg' 8 | reference: second-post 9 | --- 10 | Ik ben makelaar in koffi, en woon op de Lauriergracht No 37. Het is mijn gewoonte niet, romans te schrijven, of zulke dingen, en het heeft dan ook lang geduurd, voor ik er toe overging een paar riem papier extra te bestellen, en het werk aan te vangen, dat gij, lieve lezer, zoâven in de hand hebt genomen, en dat ge lezen moet als ge makelaar in koffie zijt, of als ge wat anders zijt. Niet alleen dat ik nooit iets schreef wat naar een roman geleek, maar ik houd er zelfs niet van, iets dergelijks te lezen, omdat ik een man van zaken ben. 11 | 12 | Sedert jaren vraag ik mij af, waartoe zulke dingen dienen, en ik sta verbaasd over de onbeschaamdheid, waarmee een dichter of romanverteller u iets op de mouw durft spelden, dat nooit gebeurd is, en meestal niet gebeuren kan.Als ik in mijn vak -- ik ben makelaar in koffie, en woon op de Lauriergracht No 37 -- aan een principaal -- een principaal is iemand die koffie verkoopt -- een opgave deed, waarin maar een klein gedeelte der onwaarheden voorkwam, die in gedichten en romans de hoofdzaak uitmaken, zou hij terstond Busselinck & Waterman nemen. 13 | 14 | Dat zijn ook makelaars in koffie, doch hun adres behoeft ge niet te weten. Ik pas er dus wel op, dat ik geen romans schrijf, of andere valse opgaven doe. Ik heb dan ook altijd opgemerkt dat mensen die zich met zoiets inlaten, gewoonlijk slecht wegkomen. Ik ben drieënveertig jaar oud, bezoek sedert twintig jaren de beurs, en kan dus voor de dag treden, als men iemand roept die ondervinding heeft. Ik heb al wat huizen zien vallen! En gewoonlijk, wanneer ik de oorzaken naging, kwam het me voor, dat die moesten gezocht worden in de verkeerde richting die aan de meesten gegeven was in hun jeugd. -------------------------------------------------------------------------------- /src/content/blog/de/erster-post.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Erster Post' 3 | description: 'Dagegen tadelt und hasst man mit Recht' 4 | pubDate: 'Jul 07 2022' 5 | previewImage: '/blog-placeholder-3.jpg' 6 | reference: first-post 7 | tags: ['Lockungen'] 8 | --- 9 | 10 | Dagegen tadelt und hasst man mit Recht Den, welcher sich durch die Lockungen einer gegenwärtigen Lust erweichen und verführen lässt, ohne in seiner blinden Begierde zu sehen, welche Schmerzen und Unannehmlichkeiten seiner deshalb warten. Gleiche Schuld treffe Die, welche aus geistiger Schwäche, d.h. um der Arbeit und dem Schmerze zu entgehen, ihre Pflichten verabsäumen. Man kann hier leicht und schnell den richtigen Unterschied treffen; zu einer ruhigen Zeit, wo die Wahl der Entscheidung völlig frei ist und nichts hindert, das zu thun, was den Meisten gefällt, hat man jede Lust zu erfassen und jeden Schmerz abzuhalten; aber zu Zeiten trifft es sich in Folge von schuldigen Pflichten oder von sachlicher Noth, dass man die Lust zurückweisen und Beschwerden nicht von sich weisen darf. Deshalb trifft der Weise dann eine Auswahl, damit er durch Zurückweisung einer Lust dafür eine grössere erlange oder durch Übernahme gewisser Schmerzen sich grössere erspare. 11 | 12 | Damit Ihr indess erkennt, woher dieser ganze Irrthum gekommen ist, und weshalb man die Lust anklagt und den Schmerz lobet, so will ich Euch Alles eröffnen und auseinander setzen, was jener Begründer der Wahrheit und gleichsam Baumeister des glücklichen Lebens selbst darüber gesagt hat. Niemand, sagt er, verschmähe, oder hasse, oder fliehe die Lust als solche, sondern weil grosse Schmerzen ihr folgen, wenn man nicht mit Vernunft ihr nachzugehen verstehe. Ebenso werde der Schmerz als solcher von Niemand geliebt, gesucht und verlangt, sondern weil mitunter solche Zeiten eintreten, dass man mittelst Arbeiten und Schmerzen eine grosse Lust sich zu verschaften suchen müsse. Um hier gleich bei dem Einfachsten stehen zu bleiben, so würde Niemand von uns anstrengende körperliche Übungen vornehmen, wenn er nicht einen Vortheil davon erwartete. Wer dürfte aber wohl Den tadeln, der nach einer Lust verlangt, welcher keine Unannehmlichkeit folgt, oder der einem Schmerze ausweicht, aus dem keine Lust hervorgeht? 13 | -------------------------------------------------------------------------------- /src/content/blog/de/zweiter-post.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Zweiter Post' 3 | author: Fatima Rodríguez 4 | description: 'Damit Ihr indess erkennt' 5 | pubDate: 'Jul 22 2022' 6 | previewImage: '/blog-placeholder-4.jpg' 7 | reference: second-post 8 | tags: [Lockungen, Verführen] 9 | --- 10 | 11 | Damit Ihr indess erkennt, woher dieser ganze Irrthum gekommen ist, und weshalb man die Lust anklagt und den Schmerz lobet, so will ich Euch Alles eröffnen und auseinander setzen, was jener Begründer der Wahrheit und gleichsam Baumeister des glücklichen Lebens selbst darüber gesagt hat. Niemand, sagt er, verschmähe, oder hasse, oder fliehe die Lust als solche, sondern weil grosse Schmerzen ihr folgen, wenn man nicht mit Vernunft ihr nachzugehen verstehe. Ebenso werde der Schmerz als solcher von Niemand geliebt, gesucht und verlangt, sondern weil mitunter solche Zeiten eintreten, dass man mittelst Arbeiten und Schmerzen eine grosse Lust sich zu verschaften suchen müsse. Um hier gleich bei dem Einfachsten stehen zu bleiben, so würde Niemand von uns anstrengende körperliche Übungen vornehmen, wenn er nicht einen Vortheil davon erwartete. Wer dürfte aber wohl Den tadeln, der nach einer Lust verlangt, welcher keine Unannehmlichkeit folgt, oder der einem Schmerze ausweicht, aus dem keine Lust hervorgeht? 12 | 13 | Dagegen tadelt und hasst man mit Recht Den, welcher sich durch die Lockungen einer gegenwärtigen Lust erweichen und verführen lässt, ohne in seiner blinden Begierde zu sehen, welche Schmerzen und Unannehmlichkeiten seiner deshalb warten. Gleiche Schuld treffe Die, welche aus geistiger Schwäche, d.h. um der Arbeit und dem Schmerze zu entgehen, ihre Pflichten verabsäumen. Man kann hier leicht und schnell den richtigen Unterschied treffen; zu einer ruhigen Zeit, wo die Wahl der Entscheidung völlig frei ist und nichts hindert, das zu thun, was den Meisten gefällt, hat man jede Lust zu erfassen und jeden Schmerz abzuhalten; aber zu Zeiten trifft es sich in Folge von schuldigen Pflichten oder von sachlicher Noth, dass man die Lust zurückweisen und Beschwerden nicht von sich weisen darf. Deshalb trifft der Weise dann eine Auswahl, damit er durch Zurückweisung einer Lust dafür eine grössere erlange oder durch Übernahme gewisser Schmerzen sich grössere erspare. 14 | -------------------------------------------------------------------------------- /src/components/Head.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { siteTitle, siteThemeColor } from '@src/consts'; 3 | import DescriptionMeta from './DescriptionMeta.astro'; 4 | import type { CommonFrontmatter } from '@src/types'; 5 | 6 | // Import the global.css file here so that it is included on 7 | // all pages through the use of the component. 8 | import '../styles/global.css'; 9 | import '@exampledev/new.css' 10 | import '../styles/prose.css'; 11 | 12 | interface Props { 13 | frontmatter: CommonFrontmatter, 14 | includeCanonicalMeta?: boolean 15 | } 16 | 17 | const 18 | mode = import.meta.env.MODE, 19 | {frontmatter, includeCanonicalMeta = true} = Astro.props, 20 | title = frontmatter.title ? `${frontmatter.title} | ${siteTitle}` : siteTitle, 21 | description = frontmatter.description, 22 | file = frontmatter.file, 23 | previewImage = frontmatter.previewImage || '/blog-placeholder-1.jpg', 24 | canonicalURL = new URL(frontmatter.canonicalURL || Astro.url.pathname, Astro.site), 25 | faviconIco = mode === 'development' ? '/favicon-dev.ico' : '/favicon.ico', 26 | faviconSVG = mode === 'development' ? '/favicon-dev.svg' : '/favicon.svg', 27 | appleTouchIcon = mode === 'development' ? '/apple-touch-icon-dev.png' : '/apple-touch-icon.png'; 28 | --- 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | {title} 45 | 46 | 47 | 48 | 49 | 50 | {includeCanonicalMeta && 51 | 52 | } 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | {description && 67 | 68 | } 69 | 70 | 71 | 72 | 73 | 74 | 75 | {description && 76 | 77 | } 78 | 79 | -------------------------------------------------------------------------------- /src/content/blog/nl/derde-post.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Derde post' 3 | author: 'David Müller' 4 | description: 'Lorem ipsum dolor sit amet' 5 | pubDate: 'Jul 28 2022' 6 | previewImage: '/blog-placeholder-3.jpg' 7 | reference: third-post 8 | tags: [Mattis, Puur] 9 | --- 10 | Lorem ipsum dolor zit amet, consectetur adipiscing elit, sed doen eiusmod tempor incididunt ut arbeid en dolore magna aliqua. Leven ultricies leo integer mannen nu vel risus behaaglijk levend. Adipiscing dus ik ben turpis egestas pretium. Euismod item nu quis eleifend quam adipiscing. In dit huis-vestibulum dictumst vestibulum. Sagittis puur nu volutpat. Netus en mannen roem ac turpis egestas. Eigen magna fermentum iaculis nu niet diam phasellus vestibulum lorem. Verschillende mannen nu mattis vulputate nu. Vestibulum dictumst vrouwen sagittis. Integer vrouwen auctor elit ik ben vulputate nu. Dictumst vrouwen sagittis puur nu. 11 | 12 | Morbi tristique ouderdom en netus. Nu risus in hendrerit gravida rutrum vrouwen niet vertellen. Vestibulum dictumst vrouwen sagittis puur nu. Vertel molestie nu niet blandit massa. Cursus vrouwen congue mauris rhoncus. Accumsan vrouwen posuere nu ut. Fringilla nu porttitor rhoncus dolore. Elite vrouwen dignissim cras tincidunt lobortis. In cursus vrouwen tincidunt nu ut ornare lectus. Integer vrouwen scelerisque varius morbi nu. Bibendum nu egestas congue vrouwen egestas diam. Cras vrouwen arcu vrouwen vivamus vrouwen felis bibendum. Dignissim opgeschorte in vrouwen ante in nibh mauris. Tempus nu et pharetra pharetra massa massa ultricies nu. 13 | 14 | Mollis nu niet nu id semper risus in. Convallis een vrouwen semper auctor nu. Diam nu amet nisl suscipit. Lacus viverra vitae vrouwen congue nu consequat vrouwen felis doen. Egestas integer eigen aliquet vrouwen praesent tristique magna nu. Eigen magna fermentum iaculis nu niet diam. In vitae vrouwen massa nu elementum. Tristique en vrouwen quis ipsum opgeschorte ultrices. Eigen lorem dolor nu viverra ipsum. Vel vrouwen nunc eigen lorem dolor nu viverra. Posuere nu ut nu consequat semper viverra nam. Laoreet opgeschorte interdum libero id faucibus. Diam phasellus vestibulum lorem nu risus ultricies tristique. Rhoncus dolore puur nu vrouwen praesent elementum facilisis. Ultrices tincidunt nu non sodales nu. Tempus egestas nu nu risus pretium vrouwen vulputate. Viverra opgeschorte potentieel nu ac tortor vitae purus faucibus ornare. Fringilla nu porttitor rhoncus dolore puur nu. Dictum nu amet justo doen in vrouwen. 15 | 16 | Mattis vrouwen velit nu vrouwen morbi nu. Tortor posuere nu ut nu consequat semper viverra. Tellus mauris een vrouwen maecenas vrouwen nu doen. Venenatis urna cursus eigen nu scelerisque viverra mauris in. Arcu een tortor vrouwen convallis een et tortor in. Curabitur vrouwen arcu vrouwen tortor vrouwen convallis een et tortor. Egestas nu rutrum nu pellentesque vrouwen. Fusce nu placeren nu nulla pellentesque vrouwen doen in. Ut vrouwen blandit volutpat maecenas volutpat vrouwen aliquam etiam. Id doen ultrices tincidunt nu. Id cursus nu aliquam eleifend nu. 17 | 18 | Tempus quam pellentesque nu vrouwen aliquam nu. Risus bij ultrices nu tempus imperdiet. Id deur nu vrouwen cras nu doen eget velit. Ipsum een vrouwen cursus vitae. Facilisis magna etiam tempus nu eu lobortis elementum. Tincidunt nu ut ornare lectus nu. Quisque nu vertellen of vrouwen ac. Blandit nu volutpat nu cras. Niet tincidunt vrouwen nu feugiat nu sed pulvinar proin vrouwen. Egestas integer eigen aliquet vrouwen praesent tristique magna nu. -------------------------------------------------------------------------------- /src/content/blog/second-post.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Second post' 3 | description: '' 4 | author: 'Jana Pulanke' 5 | pubDate: 'Jul 10 2022' 6 | previewImage: '/blog-placeholder-4.jpg' 7 | reference: second-post 8 | tags: 9 | - Consectetur 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 | previewImage: '/blog-placeholder-3.jpg' 6 | reference: first-post 7 | tags: 8 | - Consectetur 9 | - Sagittis 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. -------------------------------------------------------------------------------- /src/people.ts: -------------------------------------------------------------------------------- 1 | export type PersonData = { 2 | givenName: string 3 | surName?: string 4 | publishProfile?: boolean 5 | mail?: string 6 | mastodon?: string 7 | description: { 8 | [key: string]: string 9 | } 10 | }; 11 | 12 | export const peopleData: PersonData[] = [ 13 | { 14 | givenName: 'Ana', 15 | surName: 'Grigoryan', 16 | publishProfile: true, 17 | mail: 'ana.g@example.com', 18 | mastodon: '@ana@weirdanimal.name', 19 | description: { 20 | en: 'Ana, an accomplished author with a passion for weaving tales that resonate with the human experience, brings a unique blend of literary artistry and insightful storytelling to her work. With a background in psychology and a keen interest in exploring the complexities of relationships, Jane crafts narratives that delve into the depths of emotion and challenge readers to reflect on the intricacies of the human condition. Her novels, known for their rich character development and thought-provoking themes, have earned critical acclaim and a devoted readership. Whether exploring the nuances of love, loss, or self-discovery, Jane Mitchell’s writing transcends genres, creating an immersive and memorable reading experience for those seeking narratives that both entertain and inspire.', 21 | de: 'Ana, eine erfahrene Autorin mit Leidenschaft für das Weben von Geschichten, die mit der menschlichen Erfahrung in Resonanz stehen, bringt eine einzigartige Mischung aus literarischer Kunstfertigkeit und einfühlsamem Geschichtenerzählen in ihre Werke ein. Mit einem Hintergrund in Psychologie und einem starken Interesse daran, die Komplexitäten von Beziehungen zu erforschen, erschafft Jane Erzählungen, die in die Tiefen der Emotionen eindringen und die Leser dazu herausfordern, über die Feinheiten der menschlichen Existenz nachzudenken. Ihre Romane, bekannt für ihre facettenreiche Charakterentwicklung und nachdenklichen Themen, haben kritische Anerkennung und eine treue Leserschaft erhalten. Ob sie die Nuancen von Liebe, Verlust oder Selbstfindung erforscht, übertrifft Janes Schreiben Genregrenzen und schafft eine immersive und unvergessliche Leseerfahrung für diejenigen, die nach Erzählungen suchen, die sowohl unterhalten als auch inspirieren.', 22 | nl: 'Ana, een ervaren auteur met een passie voor het weven van verhalen die resoneren met de menselijke ervaring, brengt een unieke mix van literaire kunst en inzichtelijk vertellen naar haar werk. Met een achtergrond in psychologie en een sterke interesse in het verkennen van de complexiteiten van relaties, creëert Jane vertellingen die diep ingaan op de emoties en lezers uitdagen om na te denken over de subtiliteiten van de menselijke conditie. Haar romans, bekend om hun rijke karakterontwikkeling en doordachte thema’s, hebben kritische lof en een toegewijde lezersschare verdiend. Of ze nu de nuances van liefde, verlies of zelfontdekking verkent, overstijgt het schrijven van Jane genres en biedt een meeslepende en gedenkwaardige leeservaring voor degenen die op zoek zijn naar verhalen die zowel vermaken als inspireren.' 23 | } 24 | }, 25 | { 26 | givenName: 'Fatima', 27 | surName: 'Rodríguez', 28 | publishProfile: true, 29 | mail: '', 30 | mastodon: '@fati@difficultword.social', 31 | description: { 32 | en: '', 33 | de: '', 34 | nl: '', 35 | } 36 | }, 37 | { 38 | givenName: 'David', 39 | surName: 'Müller', 40 | mail: 'dmueller@example.com', 41 | description: { 42 | en: '', 43 | de: '', 44 | nl: '', 45 | } 46 | }, 47 | { 48 | givenName: 'Ashley', 49 | publishProfile: false, 50 | mail: 'ash@example.com', 51 | description: { 52 | en: '', 53 | de: '', 54 | nl: '', 55 | } 56 | } 57 | ] -------------------------------------------------------------------------------- /src/components/MainI18n.astro: -------------------------------------------------------------------------------- 1 | --- 2 | // Create an object with translations of the page. 3 | // A translation is identified by its 'reference' frontmatter value: pages that have the same value are considered translations. 4 | // If there are translations of the page, the TranslationsBanner is shown. 5 | // Because astro.glob() and getCollections are only available inside .astro files, the logic can't be easily moved to utility function. 6 | // The benefit of having the logic inside this component is that the translations only need to be searched once for all the places where they are used (head and TranslationsBanner). 7 | import {site} from '@src/consts' 8 | import { getLocaleFromUrl } from '@i18n/utilities'; 9 | import { uiStrings } from '@i18n/uiStrings'; 10 | import Html from "@components/Html.astro"; 11 | import TranslationsBanner from "@components/TranslationsBanner.astro"; 12 | import TranslationLinks from "@components/TranslationLinks.astro"; 13 | import Header from "@components/Header.astro"; 14 | import { getAbsolutePostPath, getRelativePostPath } from '@utilities/getPostPath'; 15 | import getPublishedPosts from '@utilities/getPublishedPosts'; 16 | import type { CommonFrontmatter } from '@src/types'; 17 | 18 | interface Props { 19 | frontmatter: CommonFrontmatter, 20 | collection: string 21 | } 22 | export interface TranslationLink{ 23 | absoluteURL: string, 24 | relativeURL: string, 25 | locale: string, 26 | label: string 27 | } 28 | 29 | const 30 | {frontmatter, collection} = Astro.props, 31 | reference: string = frontmatter?.reference || ''; 32 | 33 | let translationsAvailable = [] 34 | let translationLinks: TranslationLink[] = [] 35 | 36 | if (reference){ 37 | if (collection === 'pages'){ 38 | // The current page is not part of a collection 39 | const allPages = await Astro.glob('../pages/**/*.{md,mdx}') 40 | translationsAvailable = allPages 41 | .filter(({ frontmatter }) => { 42 | return frontmatter.reference !== null && frontmatter.reference === reference 43 | }) 44 | .filter(page => { 45 | return getLocaleFromUrl(page.url) !== Astro.currentLocale 46 | }) 47 | for (const translation of translationsAvailable){ 48 | const 49 | locale = getLocaleFromUrl(translation.url), 50 | relativeURL = translation.url || '/', 51 | absoluteURL = `${site}${relativeURL}`; 52 | const link = { 53 | absoluteURL: absoluteURL, 54 | relativeURL: relativeURL, 55 | locale: locale, 56 | label: uiStrings[locale][Astro.currentLocale] 57 | } 58 | translationLinks.push(link) 59 | } 60 | } 61 | else{ 62 | // The current page is part of a collection 63 | const translationsSlugs = (await getPublishedPosts('', collection, '')) 64 | .filter((post)=> post.data.reference === reference) 65 | .filter(function(post){ return getLocaleFromUrl(post.slug) !== Astro.currentLocale }) 66 | .map(post => (post.slug)) 67 | 68 | for (const slug of translationsSlugs){ 69 | const locale = getLocaleFromUrl(slug) 70 | const link = { 71 | absoluteURL: getAbsolutePostPath(locale, collection, slug), 72 | relativeURL: getRelativePostPath(locale, collection, slug), 73 | locale: locale, 74 | label: uiStrings[locale][Astro.currentLocale] 75 | } 76 | translationLinks.push(link) 77 | } 78 | } 79 | } 80 | --- 81 | 82 | 83 | {translationLinks.map((translation) => ( 84 | 85 | ))} 86 | 87 |
    88 | {translationLinks.length > 0 && 89 | 90 | 91 | 92 | } 93 |
    94 | 95 | {translationLinks.length > 0 && 96 |
    97 | 98 |
    99 | } 100 | -------------------------------------------------------------------------------- /src/content/blog/third-post.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Third post' 3 | author: 'Ana Grigoryan' 4 | description: '' 5 | pubDate: 'Jul 15 2022' 6 | previewImage: '/blog-placeholder-2.jpg' 7 | tags: 8 | - markdown 9 | reference: third-post 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 | ![Alt text](./full/or/relative/path/of/image) 42 | ``` 43 | 44 | #### Output 45 | 46 | ![blog placeholder](/blog-placeholder-about.jpg) 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 | Mixed list: 182 | 183 | - Fruit 184 | 1. Apple 185 | 2. Orange 186 | 3. Banana 187 | - Dairy 188 | 1. Milk 189 | 2. Cheese 190 | 191 | Mixed list: 192 | 193 | 1. Fruit 194 | - Apple 195 | - Orange 196 | - Banana 197 | 2. Dairy 198 | - Milk 199 | - Cheese 200 | ``` 201 | 202 | #### Output 203 | 204 | - Fruit 205 | - Apple 206 | - Orange 207 | - Banana 208 | - Dairy 209 | - Milk 210 | - Cheese 211 | 212 | Mixed list: 213 | 214 | - Fruit 215 | 1. Apple 216 | 2. Orange 217 | 3. Banana 218 | - Dairy 219 | 1. Milk 220 | 2. Cheese 221 | 222 | Mixed list: 223 | 224 | 1. Fruit 225 | - Apple 226 | - Orange 227 | - Banana 228 | 2. Dairy 229 | - Milk 230 | - Cheese 231 | 232 | ## Other Elements — abbr, sub, sup, kbd, mark 233 | 234 | #### Syntax 235 | 236 | ```markdown 237 | GIF is a bitmap image format. 238 | 239 | H2O 240 | 241 | Xn + Yn = Zn 242 | 243 | Press CTRL+ALT+Delete to end the session. 244 | 245 | Most salamanders are nocturnal, and hunt for insects, worms, and other small creatures. 246 | ``` 247 | 248 | #### Output 249 | 250 | GIF is a bitmap image format. 251 | 252 | H2O 253 | 254 | Xn + Yn = Zn 255 | 256 | Press CTRL+ALT+Delete to end the session. 257 | 258 | Most salamanders are nocturnal, and hunt for insects, worms, and other small creatures. 259 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Astro i18n blog starter 2 | 3 | [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) 4 | 5 | > [Live demo](https://astro-i18n-blog-starter.netlify.app/) 6 | 7 | I found that out of the box, the Astro 4.5 [blog example](https://github.com/withastro/astro/tree/latest/examples/blog) and [internationalization/localization (i18n)](https://docs.astro.build/en/guides/internationalization/) features require quite a bit of work to become a fully working, SEO-optimized and screen reader-friendly blog. This project attempts to make getting started a bit easier. 8 | 9 | > [!CAUTION] 10 | > I've stopped maintenance of this starter, because: 11 | > 1. Keeping this repo updated with Astro's changes takes quit a bit of time, whereas pulling in changes to sites that are based can likely result in merge conflicts. 12 | > 2. The way it redefines collection pages' 'slugs' (paths) using `getPagePath()` and `getPostPath()` works fine, but I've found that it's convoluted and that it's easier to use file path-based routing instead—even if that involves a slightly complicated glob pattern with the default language not having a path prefix. 13 | 14 | > [!IMPORTANT] 15 | > There's one feature you may still find interesting to see in this project: finding translations of pages. Other starters often rely on file names to find translations, making it impossible to make locale-specific URLs. In this repo you find that pages can have a `reference` frontmatter value. `MainI18n.astro` creates an array of all translations of the current page, based on that value. 16 | 17 | > [!TIP] 18 | > To create the what I called a 'slighly complicated glob pattern' above, you can define [the loader](https://docs.astro.build/en/guides/content-collections/#defining-the-collection-loader) (that's an Astro 5 thing) for the blog collection like this: 19 | > ```js 20 | > const bblogCollection = defineCollection({ 21 | > loader: glob({ 22 | > pattern: "{blog,nl/blog,de/blog}/**/*.{md,mdx}", 23 | > base: "./src/content/", 24 | > }), 25 | > // ... 26 | > }) 27 | > ``` 28 | 29 | ## Requirements and goals 30 | 31 | - Use built-in features and reduce additional dependencies where possible. The assumption is that the Astro team will expand i18n features and that parts of this setup can be replaced with built-in functions in the future. 32 | - Add a minimum of styling and hard-coded settings, so you can quickly get to styling and configuring your own site. 33 | - Allow for multiple collections (don't abuse collections for localization) 34 | - Separate layouts, components and content; make sure all content is saved in markdown and data files. 35 | - Support the features below! 36 | 37 | ## Features 38 | 39 | - ⛓️ Linked translations via a reference property: no need for matching slugs between locales. 40 | - 🏖️ Allow adding pages for a new locale with minimal effort; use the default locale* as a fallback for missing settings and strings. 41 | - 🏷️ Content tags á la WordPress 42 | - 🗺️ Sitemap support with translation links 43 | - 📡 Localized RSS Feeds 44 | - 🌍 Customizable URL structure, like `domain.tld/locale/directory/slug` 45 | - 🪽 Skip to content link for screen reader and keyboard users 46 | - 👩‍💼 Localized author profiles from a single data file 47 | - 🔏 Secret/draft state to exclude posts from rendering 48 | - 🔚 404 Page not found page 49 | - 🐭 Ultra minimal styling without CSS classes with [new.css](https://newcss.net/) (remove only two lines of code to remove it!) 50 | - 🔗 `target="_blank"` for external links with [Rehype plugin](https://github.com/rehypejs/rehype-external-links) 51 | - 😉 Separate favicon for dev server to not get confused between dev and production 52 | 53 | *) This is why the Dutch demo pages have an English header menu; no Dutch menu items were defined. 54 | 55 | Based on the official [blog example](https://github.com/withastro/astro/tree/latest/examples/blog), this setup still has: 56 | 57 | - ✅ 100/100 Lighthouse performance 58 | - ✅ SEO-friendly with canonical URLs and OpenGraph data 59 | - ✅ Markdown & MDX support 60 | 61 | ## 🚀 Project Structure 62 | 63 | You'll see the following folders and files: 64 | 65 | ```text 66 | ├── public/ 67 | ├── src/ 68 | │   ├── components/ 69 | │   ├── content/ 70 | │   ├── i18n/ 71 | │   │ ├── i18n.ts ← Set up locales here 72 | │   │ ├── uiStrings.js ← Localized headings, labels, etc. 73 | │   │ └── utilities ← i18n-specific functions 74 | │   ├── layouts/ 75 | │   ├── styles/ 76 | │   ├── utilities/ 77 | │   ├── consts.ts ← Settings loaded by astro.config.mjs 78 | │   ├── env.d.ts 79 | │   ├── header.ts ← Settings for header menus, optionally per locale 80 | │   └── people.ts ← Bylines and author profile pages 81 | ├── astro.config.mjs 82 | ├── README.md 83 | ├── package.json 84 | └── tsconfig.json 85 | ``` 86 | 87 | Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. 88 | 89 | There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. 90 | 91 | The `src/content/` directory contains collections of related markdown and MDX documents. A 'blog' collection has been defined already. 92 | 93 | Any static assets, like images, can be placed in the `public/` directory. 94 | 95 | ## 🧞 Commands 96 | 97 | All default commands can be run from the root of the project, from a terminal: 98 | 99 | | Command | Action | 100 | | :------------------------- | :----------------------------------------------- | 101 | | `pnpm install` | Installs dependencies | 102 | | `pnpm run dev` | Starts local dev server at `localhost:4321` | 103 | | `pnpm run build` | Build your production site to `./dist/` | 104 | | `pnpm run preview` | Preview your build locally, before deploying | 105 | | `pnpm run astro ...` | Run CLI commands like `astro add`, `astro check` | 106 | | `pnpm run astro -- --help` | Get help using the Astro CLI | 107 | 108 | ## To do 109 | 110 | - [ ] Get feedback. This is my third Astro project, but it's I noticed while making this, that I'm very much a novice Astro user. Feel free to [contact me](https://www.kooslooijesteijn.net/contact) and make pull requests. 111 | - [ ] Although there are no errors or known issues, your editor may show a few squiggly lines caused missing/faulty TypeScript settings. 112 | --------------------------------------------------------------------------------