├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── SECURITY.md └── workflows │ ├── cd.yaml │ └── ci.yaml ├── .gitignore ├── .prettierrc.js ├── .stylelintignore ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── astro.config.ts ├── content ├── posts │ ├── color-schemes.mdx │ ├── configuring-nordlys.md │ ├── syntax-highlighting.md │ ├── updating.md │ ├── using-images.mdx │ └── writing-content.md └── projects │ ├── example-project.md │ └── nordlys.md ├── eslint.config.js ├── package.json ├── pnpm-lock.yaml ├── public ├── favicon.svg ├── preview.png └── zoom-vanilla.js │ ├── zoom-vanilla.min.js │ └── zoom.css ├── src ├── assets │ ├── logo.svg │ └── screenshots │ │ ├── aurora-dark-landing.png │ │ ├── aurora-dark-projects.png │ │ ├── aurora-light-landing.png │ │ ├── aurora-light-projects.png │ │ ├── mono-dark-landing.png │ │ ├── mono-dark-projects.png │ │ ├── mono-light-landing.png │ │ ├── mono-light-projects.png │ │ ├── nord-dark-landing.png │ │ ├── nord-dark-projects.png │ │ ├── nord-light-landing.png │ │ └── nord-light-projects.png ├── components │ ├── AdjacentPostsBar.astro │ ├── Dropdown.astro │ ├── Hero.astro │ ├── Pagination.astro │ ├── PostPreview.astro │ ├── PostsList.astro │ ├── Project.astro │ ├── ProjectsList.astro │ ├── TagsBar.astro │ ├── ToC.astro │ ├── layout │ │ ├── Footer.astro │ │ ├── FooterItem.astro │ │ ├── Header.astro │ │ ├── HeaderItem.astro │ │ ├── MobileNavToggle.astro │ │ ├── Prose.astro │ │ └── Separator.astro │ └── mode │ │ ├── ModeManager.astro │ │ └── ModeToggle.astro ├── content.config.ts ├── env.d.ts ├── layouts │ ├── BaseLayout.astro │ ├── LandingLayout.astro │ ├── PageLayout.astro │ └── PostLayout.astro ├── ogImages │ ├── extractColorScheme.ts │ ├── index.ts │ ├── post.ts │ ├── site.ts │ └── urlEncodedLogo.ts ├── pages │ ├── 404.md │ ├── about.mdx │ ├── authors │ │ └── [author].astro │ ├── feed.xml.ts │ ├── index.md │ ├── ogImage.png.ts │ ├── posts │ │ ├── [...id].png.ts │ │ ├── [...id] │ │ │ └── index.astro │ │ ├── [...page].astro │ │ └── [page].astro │ ├── projects │ │ ├── [...page].astro │ │ └── [page].astro │ ├── robots.txt.ts │ ├── search.astro │ └── tags │ │ ├── [tag].astro │ │ └── index.astro ├── plugins │ ├── CopyCodeButtonsPlugin.astro │ ├── HeadingAnchorsPlugin.astro │ ├── PagefindPlugin.astro │ ├── ScrollProgressPlugin.astro │ ├── ScrollToTopPlugin.astro │ ├── codeHeadersPlugin.ts │ └── readingTimePlugin.ts ├── style │ ├── astro-code.css │ ├── color-schemes.css │ ├── main.css │ └── theme.css ├── theme.config.ts ├── types.ts └── util │ ├── index.ts │ ├── posts.ts │ ├── projects.ts │ └── tags.ts ├── stylelint.config.js └── tsconfig.json /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement. 63 | All complaints will be reviewed and investigated promptly and fairly. 64 | 65 | All community leaders are obligated to respect the privacy and security of the 66 | reporter of any incident. 67 | 68 | ## Enforcement Guidelines 69 | 70 | Community leaders will follow these Community Impact Guidelines in determining 71 | the consequences for any action they deem in violation of this Code of Conduct: 72 | 73 | ### 1. Correction 74 | 75 | **Community Impact**: Use of inappropriate language or other behavior deemed 76 | unprofessional or unwelcome in the community. 77 | 78 | **Consequence**: A private, written warning from community leaders, providing 79 | clarity around the nature of the violation and an explanation of why the 80 | behavior was inappropriate. A public apology may be requested. 81 | 82 | ### 2. Warning 83 | 84 | **Community Impact**: A violation through a single incident or series 85 | of actions. 86 | 87 | **Consequence**: A warning with consequences for continued behavior. No 88 | interaction with the people involved, including unsolicited interaction with 89 | those enforcing the Code of Conduct, for a specified period of time. This 90 | includes avoiding interactions in community spaces as well as external channels 91 | like social media. Violating these terms may lead to a temporary or 92 | permanent ban. 93 | 94 | ### 3. Temporary Ban 95 | 96 | **Community Impact**: A serious violation of community standards, including 97 | sustained inappropriate behavior. 98 | 99 | **Consequence**: A temporary ban from any sort of interaction or public 100 | communication with the community for a specified period of time. No public or 101 | private interaction with the people involved, including unsolicited interaction 102 | with those enforcing the Code of Conduct, is allowed during this period. 103 | Violating these terms may lead to a permanent ban. 104 | 105 | ### 4. Permanent Ban 106 | 107 | **Community Impact**: Demonstrating a pattern of violation of community 108 | standards, including sustained inappropriate behavior, harassment of an 109 | individual, or aggression toward or disparagement of classes of individuals. 110 | 111 | **Consequence**: A permanent ban from any sort of public interaction within 112 | the community. 113 | 114 | ## Attribution 115 | 116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 117 | version 2.0, available at 118 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 119 | 120 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 121 | enforcement ladder](https://github.com/mozilla/diversity). 122 | 123 | [homepage]: https://www.contributor-covenant.org 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | https://www.contributor-covenant.org/faq. Translations are available at 127 | https://www.contributor-covenant.org/translations. 128 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for your interest in contributing to this project! Every effort is appreciated! 4 | 5 | ## Documentation 6 | 7 | This theme is self-documenting, meaning there are no separate docs; all relevant features, configuration options, and examples are provided in blog posts within the theme. If you have improvements, valuable hints, or additions, you are welcome to open a PR to help grow the documentation! 8 | 9 | ## Issues 10 | 11 | You can report bugs or submit feature requests by [opening an issue](https://github.com/FjellOverflow/nordlys/issues/new/choose). Before submitting, please check that a similar issue has not already been filed. 12 | 13 | ## Pull requests 14 | 15 | Pull requests are welcome, whether they involve a bug fix, a new feature, or any kind of improvement! 16 | 17 | Before submitting, please ensure that everything still works as expected. If not, make sure to point out the breaking changes in the PR. 18 | 19 | In any case, write a brief sentence about your changes and reasoning, and run `pnpm check` to verify the absence of Type and Linter errors. 20 | 21 | If you implement a larger change or feature, consider whether it is necessary to update the documentation (blog posts) or add a new entry to inform users of your changes. 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Summarize the proposed changes** 2 | Please include a summary of the submitted changes. Explain their purpose or reference which issue is fixed. Please also include relevant motivation and context. List all added/removed/updated dependencies included in this change. 3 | 4 | **What is the type of change? Select one or more.** 5 | - [ ] Bug fix (non-breaking change which fixes an issue) 6 | - [ ] New feature (non-breaking change which adds functionality) 7 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 8 | - [ ] Other (please describe) 9 | 10 | **Is there any action needed after merging?** 11 | Please give a quick overview of required actions after adding the new changes, such as updating documentation, adding new tests, or other, if there are any. 12 | 13 | **Please make sure the PR complies with this checklist** 14 | - [ ] The change has been tested locally 15 | - [ ] The code has been linted and type-checked 16 | - [ ] All relevant tests have been run and passed 17 | - [ ] There is currently no merge conflict 18 | - [ ] No new warnings or errors have appeared (if intended, please describe) 19 | 20 | **Additional context** 21 | Add any other context about the pull request here. 22 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Please report any vulnerabilities by opening a [new issue](https://github.com/FjellOverflow/VerMon/issues/new?template=bug_report.md). 6 | -------------------------------------------------------------------------------- /.github/workflows/cd.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Build and deploy 3 | 4 | permissions: 5 | contents: read 6 | pages: write 7 | id-token: write 8 | 9 | on: 10 | push: 11 | branches: main 12 | workflow_dispatch: 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v4 20 | - name: Install, build, and upload site 21 | uses: withastro/action@v3 22 | with: 23 | node-version: 22 24 | 25 | deploy: 26 | needs: build 27 | runs-on: ubuntu-latest 28 | environment: 29 | name: github-pages 30 | url: ${{ steps.deployment.outputs.page_url }} 31 | steps: 32 | - name: Deploy to GitHub Pages 33 | uses: actions/deploy-pages@v4 34 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Run checks 3 | 4 | on: 5 | push: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | run-checks: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v4 14 | 15 | - name: Setup pnpm 16 | uses: pnpm/action-setup@v4 17 | 18 | - name: Install dependencies 19 | run: pnpm i --frozen-lockfile 20 | 21 | - name: Run checks 22 | run: pnpm check 23 | -------------------------------------------------------------------------------- /.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 | 23 | # jetbrains setting folder 24 | .idea/ 25 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import("prettier").Config} */ 2 | export default { 3 | plugins: ['prettier-plugin-astro', 'prettier-plugin-tailwindcss'], 4 | overrides: [ 5 | { 6 | files: '*.astro', 7 | options: { 8 | parser: 'astro' 9 | } 10 | } 11 | ], 12 | semi: false, 13 | singleQuote: true, 14 | trailingComma: 'none' 15 | } 16 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | **/*.js 2 | **/*.ts 3 | **/*.md 4 | **/*.mdx 5 | **/*.png 6 | **/*.jpg 7 | **/*.jpeg 8 | **/*.webp 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "astro-build.astro-vscode", 4 | "dbaeumer.vscode-eslint", 5 | "stylelint.vscode-stylelint", 6 | "bradlc.vscode-tailwindcss", 7 | "unifiedjs.vscode-mdx", 8 | "esbenp.prettier-vscode" 9 | ], 10 | "unwantedRecommendations": [] 11 | } -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "explorer.fileNesting.enabled": true, 3 | "explorer.fileNesting.patterns": { 4 | "eslint.config.js": ".prettierrc.js, stylelint.config.js, .stylelintignore", 5 | "package.json": "pnpm-lock.yaml, .gitignore", 6 | "astro.config.ts": "tsconfig.json", 7 | "README.md": "README.md, LICENSE.md, CHANGELOG.md" 8 | }, 9 | "editor.formatOnSave": true, 10 | "editor.codeActionsOnSave": { 11 | "source.fixAll.eslint": "explicit", 12 | "source.fixAll.stylelint": "explicit", 13 | "source.organizeImports": "explicit" 14 | }, 15 | "[json]": { 16 | "editor.defaultFormatter": "vscode.json-language-features" 17 | }, 18 | "[jsonc]": { 19 | "editor.defaultFormatter": "vscode.json-language-features" 20 | }, 21 | // ESLint 22 | "eslint.format.enable": true, 23 | "[javascript]": { 24 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 25 | }, 26 | "[typescript]": { 27 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 28 | "editor.autoClosingBrackets": "always" 29 | }, 30 | "[markdown]": { 31 | "editor.defaultFormatter": "esbenp.prettier-vscode" 32 | }, 33 | "[astro]": { 34 | "editor.defaultFormatter": "astro-build.astro-vscode" 35 | }, 36 | "eslint.validate": [ 37 | "javascript", 38 | "javascriptreact", 39 | "typescript", 40 | "typescriptreact", 41 | "html", 42 | "markdown", 43 | "astro" 44 | ], 45 | // Stylelint 46 | "css.validate": false, 47 | "less.validate": false, 48 | "scss.validate": false, 49 | "[css]": { 50 | "editor.defaultFormatter": "esbenp.prettier-vscode" 51 | }, 52 | "stylelint.validate": [ 53 | "html", 54 | "css", 55 | ], 56 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines. 4 | 5 | ## [2.3.2](https://github.com/FjellOverflow/nordlys/compare/v2.3.1...v2.3.2) (2025-05-04) 6 | 7 | ## [2.3.1](https://github.com/FjellOverflow/nordlys/compare/v2.3.0...v2.3.1) (2025-05-04) 8 | 9 | ### Bug Fixes 10 | 11 | - **seo:** redirect /posts & /projects to /posts/1 & /projects/1 for SEO optimization ([0c942ad](https://github.com/FjellOverflow/nordlys/commit/0c942ade3f1dbb445a13946a77d6bf8ec2cfe22b)) 12 | 13 | ## [2.3.0](https://github.com/FjellOverflow/nordlys/compare/v2.2.0...v2.3.0) (2025-03-22) 14 | 15 | ### ⚠ BREAKING CHANGES 16 | 17 | For medium-zoom on images, a new `data-action="zoom"` attribute has been introduced. That means images with the `data-img-embed` attributes will no longer be zoomable; to fix this you need to add the addtional attribute `data-action="zoom"` to your images. 18 | 19 | ### Features 20 | 21 | - make project startDate optional, sort projects without to list bottom ([54fe562](https://github.com/FjellOverflow/nordlys/commit/54fe562662ba14753185c5dedd109c817f8c8da6)) 22 | 23 | ### Bug Fixes 24 | 25 | - add missing trailing slash to canonicalURL ([acedf7e](https://github.com/FjellOverflow/nordlys/commit/acedf7e03707e382a5a9478c8c8ab2dcc1351e27)) 26 | - correct dark mode colors for syntax highlighted blox ([c720e71](https://github.com/FjellOverflow/nordlys/commit/c720e717031d3edeadf07fc8d0b3e147ace9dbba)) 27 | - switch medium-zoom lib for better Safari browser support ([d3cc91b](https://github.com/FjellOverflow/nordlys/commit/d3cc91b551fffd4fafd9eeb0d321191a7ea5dd6d)) 28 | 29 | ## [2.2.0](https://github.com/FjellOverflow/nordlys/compare/v2.1.3...v2.2.0) (2025-03-07) 30 | 31 | ### Features 32 | 33 | - responsive margins for zoomed images ([9662d15](https://github.com/FjellOverflow/nordlys/commit/9662d159c2802cda74ab40bbdc15550b4034dd8a)) 34 | 35 | ### Bug Fixes 36 | 37 | - bump @tailwindcss/vite and load missing font ([9880b84](https://github.com/FjellOverflow/nordlys/commit/9880b84683c0955567229451e2bc57de04bf9570)) 38 | - remove unnecessary 404 link ([2a762b2](https://github.com/FjellOverflow/nordlys/commit/2a762b25cd7eae3c8fe1a6f9401fddef6f24ae05)) 39 | 40 | ## [2.1.3](https://github.com/FjellOverflow/nordlys/compare/v2.1.2...v2.1.3) (2025-02-27) 41 | 42 | ### Bug Fixes 43 | 44 | - load fonts from local files during OG image generation ([1874e64](https://github.com/FjellOverflow/nordlys/commit/1874e64564923761fa0aa8f1abf30dfad284659b)) 45 | 46 | ## [2.1.2](https://github.com/FjellOverflow/nordlys/compare/v2.1.1...v2.1.2) (2025-02-26) 47 | 48 | ### Bug Fixes 49 | 50 | - add missing trailing slashes to internal links ([8c189bd](https://github.com/FjellOverflow/nordlys/commit/8c189bd975fbbd5b3981dd5e0552497fcfccd3df)) 51 | - post preview image margins ([07d2ec8](https://github.com/FjellOverflow/nordlys/commit/07d2ec83d254a86618d566d6ec00948c37022d2b)) 52 | - prevent duplicate registration of already registered custom elements ([bf390c8](https://github.com/FjellOverflow/nordlys/commit/bf390c87ea9ad6dcefa2a35c0e637ef9530d008c)) 53 | 54 | ## [2.1.1](https://github.com/FjellOverflow/nordlys/compare/v2.1.0...v2.1.1) (2025-02-25) 55 | 56 | ### Bug Fixes 57 | 58 | - **seo:** link to canonical URLs with trailing slash ([2dbb430](https://github.com/FjellOverflow/nordlys/commit/2dbb4300d750e0aa49e04ed010e573146b18cc8e)) 59 | 60 | ## [2.1.0](https://github.com/FjellOverflow/nordlys/compare/v2.0.3...v2.1.0) (2025-01-25) 61 | 62 | ### Features 63 | 64 | - scrollable ToC ([7ea348d](https://github.com/FjellOverflow/nordlys/commit/7ea348d004900ae03f0a3199d69381330837d0c5)) by @patrickpiedad 65 | 66 | ### Bug Fixes 67 | 68 | - add stylelint exception for tailwinds `@apply` ([cd95918](https://github.com/FjellOverflow/nordlys/commit/cd9591846a90c9b9aaf7252b454864b42c36d4c0)) 69 | - post og-image generation ([11df323](https://github.com/FjellOverflow/nordlys/commit/11df323c7418ceb5937b0d63ad5a68398c384788)) 70 | 71 | Big thanks to @patrickpiedad his much appreciated contribution to the project 🥳! 72 | 73 | ## [2.0.3](https://github.com/FjellOverflow/nordlys/compare/v2.0.2...v2.0.3) (2025-01-17) 74 | 75 | ### Bug Fixes 76 | 77 | - remove public image referencing from docs post ([53bb078](https://github.com/FjellOverflow/nordlys/commit/53bb078b9b1d431a5258cd754f38a192ba11145e)) 78 | 79 | ## [2.0.2](https://github.com/FjellOverflow/nordlys/compare/v2.0.1...v2.0.2) (2025-01-07) 80 | 81 | ### Bug Fixes 82 | 83 | - date range entirely right in project preview ([1c3fdbf](https://github.com/FjellOverflow/nordlys/commit/1c3fdbf1a1117ee53f6bfc4aef9636f1aabd5f77)) 84 | - require `previewImage` and `opengraphImage`to always be image in content collections ([642e142](https://github.com/FjellOverflow/nordlys/commit/642e142706084514707f1b00e90bba9940b6d2da)) 85 | 86 | ## [2.0.1](https://github.com/FjellOverflow/nordlys/compare/v2.0.0...v2.0.1) (2024-12-07) 87 | 88 | ### Bug Fixes 89 | 90 | - exclude arbitrary pages from being indexed by pagefind ([c08dd1c](https://github.com/FjellOverflow/nordlys/commit/c08dd1c71eaa0d2f3c0e66914d6fa35d68231166)) 91 | - readd local image support to Project content schema ([194ce9c](https://github.com/FjellOverflow/nordlys/commit/194ce9c03d7c8fc46a1bdcb96f4474755b44a2a6)) 92 | 93 | ## [2.0.0](https://github.com/FjellOverflow/nordlys/compare/v1.2.0...v2.0.0) (2024-12-06) 94 | 95 | Although this is a new major release, there is no "actual" changes in terms of new features or looks, but only the migration to newly released Astro 5. This entails some smaller changes under the hood, mainly due to the new [Content Layer API](https://docs.astro.build/en/guides/upgrade-to/v5/#updating-existing-collections). For anyone migrating manually, notice that the `src/content` directory moved to `content` and the new/updated `content.config.ts`. 96 | 97 | ### ⚠ BREAKING CHANGES 98 | 99 | - migrate to Astro 5 ([400d297](https://github.com/FjellOverflow/nordlys/commit/400d29756fed41591a8ffefdd8a9497070ccba83)) 100 | 101 | ### Bug Fixes 102 | 103 | - rewrite codeHeadersPlugin without postProcess shiki hook ([3fc4fe3](https://github.com/FjellOverflow/nordlys/commit/3fc4fe32e7fa1e44efbc066ecfbe83e87c8b56ff)) 104 | 105 | ## [1.2.0](https://github.com/FjellOverflow/nordlys/compare/v1.1.2...v1.2.0) (2024-12-04) 106 | 107 | ### Features 108 | 109 | - load og image logo from urlEncoded local SVG ([8fd1668](https://github.com/FjellOverflow/nordlys/commit/8fd16682540c3663a2ed79aab1d6c968811ceede)), closes [#2](https://github.com/FjellOverflow/nordlys/issues/2) 110 | 111 | ## [1.1.2](https://github.com/FjellOverflow/nordlys/compare/v1.1.1...v1.1.2) (2024-12-02) 112 | 113 | ### Bug Fixes 114 | 115 | - inline cursive text overlaps into following text ([fc0184a](https://github.com/FjellOverflow/nordlys/commit/fc0184ab2f87aaa5e05bc73e180b5866f23c02bd)) 116 | - remove scrollbar styling to restore browser default ([dfa33f0](https://github.com/FjellOverflow/nordlys/commit/dfa33f0cdcc32e9946e608a5052f3f6c8c13d8f6)), closes [#5](https://github.com/FjellOverflow/nordlys/issues/5) 117 | 118 | ## [1.1.1](https://github.com/FjellOverflow/nordlys/compare/v1.1.0...v1.1.1) (2024-11-21) 119 | 120 | ### Bug Fixes 121 | 122 | - hero background blur too wide on Opera ([a44b952](https://github.com/FjellOverflow/nordlys/commit/a44b9528b1c362dde7e6a05a15e09cd6c37ad7be)) 123 | - project preview images too wide on Safari & Firefox ([958cc3d](https://github.com/FjellOverflow/nordlys/commit/958cc3d7ba8824b6191e5992a07cb680a6b5ec9d)) 124 | 125 | ## [1.1.0](https://github.com/FjellOverflow/nordlys/compare/v1.0.0...v1.1.0) (2024-11-19) 126 | 127 | ### Features 128 | 129 | - optimize logo/favicon SVG ([4d9480b](https://github.com/FjellOverflow/nordlys/commit/4d9480b4110d2893b17490ff5864e8123c793781)) 130 | 131 | ### Bug Fixes 132 | 133 | - copy-code buttons wouldnt copy ([ec7d683](https://github.com/FjellOverflow/nordlys/commit/ec7d683069c9be27383bf3e97b12ab05aa78686a)) 134 | 135 | ## [1.0.0](https://github.com/FjellOverflow/nordlys/compare/v0.2.5...v1.0.0) (2024-11-15) 136 | 137 | The last couple of weeks I have been tweaking and improving this theme, dealt with accessibility and optimization with only few new features and changes to the outside appearance. For now I am satisfied and am both comfortable and excited to release **Nordlys 1.0.0**! As before, I am still happy and grateful for bug reports, feature requests or other contributions! 138 | 139 | ### Features 140 | 141 | - **a11y:** hidden "Skip to main content" button for keyboard-tab navigation ([3f45765](https://github.com/FjellOverflow/nordlys/commit/3f45765147acd0cd31b34d6c51ff0eca6413f30f)) 142 | - optimized post/project preview images ([5bb6939](https://github.com/FjellOverflow/nordlys/commit/5bb69390df4913873e3a2548954280867e27f425)) 143 | 144 | ### Bug Fixes 145 | 146 | - **a11y:** Make copy code icon a ` 12 | 13 | 14 | 52 | -------------------------------------------------------------------------------- /src/components/layout/Prose.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const { class: className, ...rest } = Astro.props 3 | --- 4 | 5 |
9 | 10 |
11 | 12 | 20 | -------------------------------------------------------------------------------- /src/components/layout/Separator.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const { class: className, ...rest } = Astro.props 3 | --- 4 | 5 |
6 | -------------------------------------------------------------------------------- /src/components/mode/ModeManager.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import config from '@/theme.config' 3 | 4 | const defaultMode = config.mode 5 | --- 6 | 7 | 41 | 46 | -------------------------------------------------------------------------------- /src/components/mode/ModeToggle.astro: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 32 | -------------------------------------------------------------------------------- /src/content.config.ts: -------------------------------------------------------------------------------- 1 | import config from '@/theme.config' 2 | import { glob } from 'astro/loaders' 3 | import { defineCollection, z } from 'astro:content' 4 | 5 | const posts = defineCollection({ 6 | loader: glob({ pattern: '**/[^_]*.{md,mdx}', base: './content/posts' }), 7 | schema: ({ image }) => 8 | z.object({ 9 | title: z.string(), 10 | author: z.string().default(config.author), 11 | description: z.string(), 12 | publishedDate: z.date(), 13 | draft: z.boolean().optional().default(false), 14 | canonicalURL: z.string().optional(), 15 | openGraphImage: image().optional(), 16 | tags: z.array(z.string()).default([]), 17 | showToC: z.boolean().optional().default(true), 18 | previewImage: image().optional() 19 | }) 20 | }) 21 | 22 | const projects = defineCollection({ 23 | loader: glob({ 24 | pattern: '**/[^_]*.{md,mdx}', 25 | base: './content/projects' 26 | }), 27 | schema: ({ image }) => 28 | z.object({ 29 | title: z.string(), 30 | url: z.string().optional(), 31 | startDate: z.date().optional().nullable(), 32 | endDate: z.date().optional().nullable(), 33 | tags: z.array(z.string()).default([]), 34 | previewImage: image().optional() 35 | }) 36 | }) 37 | 38 | export const collections = { posts, projects } 39 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | mode: { 3 | setMode: (mode: 'dark' | 'light') => void 4 | getMode: () => 'dark' | 'light' 5 | } 6 | 7 | pagefind: { 8 | search: (query: string) => Promise<{ results: unknown[] }> 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/layouts/BaseLayout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import '@/style/main.css' 3 | import ibmPlexNormal from '@fontsource/ibm-plex-sans/files/ibm-plex-sans-latin-400-normal.woff2?url' 4 | import ibmPlexMedium from '@fontsource/ibm-plex-sans/files/ibm-plex-sans-latin-500-normal.woff2?url' 5 | import ibmPlexSemiBold from '@fontsource/ibm-plex-sans/files/ibm-plex-sans-latin-600-normal.woff2?url' 6 | import ibmPlexBold from '@fontsource/ibm-plex-sans/files/ibm-plex-sans-latin-700-normal.woff2?url' 7 | import '@fontsource/ibm-plex-sans/latin.css' 8 | 9 | import ModeManager from '@/components/mode/ModeManager.astro' 10 | import CopyCodeButtonsPlugin from '@/plugins/CopyCodeButtonsPlugin.astro' 11 | import config from '@/theme.config' 12 | import { resolveImageUrl } from '@/util' 13 | import type { ImageMetadata } from 'astro' 14 | import { ClientRouter } from 'astro:transitions' 15 | 16 | const preloadFonts = [ 17 | ibmPlexNormal, 18 | ibmPlexMedium, 19 | ibmPlexSemiBold, 20 | ibmPlexBold 21 | ] 22 | 23 | export interface Props { 24 | frontmatter: Partial<{ 25 | title: string 26 | author: string 27 | description: string 28 | canonicalURL: string 29 | openGraphImage: string | ImageMetadata 30 | publishedDate: Date 31 | }> 32 | } 33 | 34 | const { 35 | frontmatter: { 36 | title = config.title, 37 | author = config.author, 38 | description = config.description, 39 | canonicalURL = new URL(Astro.url.pathname, Astro.site).href, 40 | openGraphImage = config.openGraphImage || '/ogImage.png', 41 | publishedDate 42 | } 43 | } = Astro.props 44 | 45 | let titleWithSuffix = title 46 | 47 | if (String(title).toLowerCase() !== String(config.title).toLowerCase()) 48 | titleWithSuffix += ` | ${config.title}` 49 | 50 | const openGraphImageUrl = new URL(resolveImageUrl(openGraphImage), Astro.site) 51 | .href 52 | --- 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | {titleWithSuffix} 65 | 66 | 67 | 68 | 69 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | { 88 | preloadFonts.map((font) => ( 89 | 96 | )) 97 | } 98 | 99 | { 100 | publishedDate && ( 101 | 105 | ) 106 | } 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /src/layouts/LandingLayout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Hero, { type Props as HeroProps } from '@/components/Hero.astro' 3 | import FooterItem from '@/components/layout/FooterItem.astro' 4 | import Separator from '@/components/layout/Separator.astro' 5 | import ModeToggle from '@/components/mode/ModeToggle.astro' 6 | import BaseLayout, { 7 | type Props as BaseLayoutProps 8 | } from '@/layouts/BaseLayout.astro' 9 | import config from '@/theme.config' 10 | 11 | export type Props = BaseLayoutProps & HeroProps 12 | 13 | const { frontmatter } = Astro.props 14 | --- 15 | 16 | 17 |
20 |
21 |
22 | {frontmatter.title && {frontmatter.title}} 23 | 29 |
30 |
31 | 32 |
35 | {config.footerItems.map((item) => )} 36 | 37 |
38 | 39 | {config.modeToggle && } 40 |
41 |
42 |
43 |
44 |
45 | 69 | -------------------------------------------------------------------------------- /src/layouts/PageLayout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Footer from '@/components/layout/Footer.astro' 3 | import Header from '@/components/layout/Header.astro' 4 | import Prose from '@/components/layout/Prose.astro' 5 | import BaseLayout, { 6 | type Props as BaseLayoutProps 7 | } from '@/layouts/BaseLayout.astro' 8 | import ScrollProgressPlugin from '@/plugins/ScrollProgressPlugin.astro' 9 | import ScrollToTopPlugin from '@/plugins/ScrollToTopPlugin.astro' 10 | import config from '@/theme.config' 11 | 12 | export type Props = BaseLayoutProps & { 13 | frontmatter: Partial<{ 14 | scrollProgress: boolean 15 | activeHeaderLink: string 16 | scrollToTop: boolean 17 | searchable: boolean 18 | }> 19 | } 20 | 21 | const { frontmatter } = Astro.props 22 | 23 | const { 24 | scrollProgress = config.scrollProgress, 25 | scrollToTop = config.scrollToTop, 26 | activeHeaderLink, 27 | searchable = false 28 | } = frontmatter 29 | --- 30 | 31 | 32 |
35 |
36 | 37 | 38 | 43 | 44 |
49 | 50 | 51 | 52 |
53 | 54 |
55 |
56 | {scrollProgress && } 57 | {scrollToTop && } 58 |
59 | -------------------------------------------------------------------------------- /src/layouts/PostLayout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import AdjacentPostsBar from '@/components/AdjacentPostsBar.astro' 3 | import TagsBar from '@/components/TagsBar.astro' 4 | import ToC from '@/components/ToC.astro' 5 | import PageLayout, { 6 | type Props as PageLayoutProps 7 | } from '@/layouts/PageLayout.astro' 8 | import HeadingAnchorsPlugin from '@/plugins/HeadingAnchorsPlugin.astro' 9 | import { toDateString } from '@/util' 10 | import { resolveTags } from '@/util/tags' 11 | import { render, type CollectionEntry } from 'astro:content' 12 | 13 | export interface Props { 14 | post: CollectionEntry<'posts'> 15 | } 16 | 17 | const { post } = Astro.props 18 | 19 | const { 20 | Content, 21 | remarkPluginFrontmatter: { readingTime }, 22 | headings 23 | } = await render(post) 24 | 25 | const frontmatter: PageLayoutProps['frontmatter'] = { 26 | ...post.data, 27 | openGraphImage: post.data.openGraphImage || `/posts/${post.id}.png`, 28 | activeHeaderLink: 'Blog', 29 | scrollProgress: true 30 | } 31 | --- 32 | 33 | 34 | 40 | 41 |

{frontmatter.title}

42 | 43 | 44 |
45 | Published on by 50 | {post.data.author} · {readingTime} 53 |
54 | 55 | { 56 | !!post.data.showToC && ( 57 | 63 | ) 64 | }{ 65 | !!post.data.showToC && ( 66 |
67 | 68 |
69 | ) 70 | } 71 | 72 |
73 | 74 |
75 | 76 | 77 | 78 |
79 | -------------------------------------------------------------------------------- /src/ogImages/extractColorScheme.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | 3 | const SCHEMES_FILE = './src/style/color-schemes.css' 4 | 5 | const lightRegex = (scheme: string) => 6 | new RegExp( 7 | `\\.${scheme}\\s*\\{\\s*--accent:\\s*([\\d\\s,]+);\\s*--accent-bg:\\s*([\\d\\s,]+);` 8 | ) 9 | 10 | const darkRegex = (scheme: string) => 11 | new RegExp( 12 | `\\.${scheme}\\s*{(?:[^{}]*{[^{}]*})*[^{}]*\\[data-mode=['"]dark['"]\\]\\s*{[^{}]*--accent:\\s*([\\d\\s,]+);[^{}]*--accent-bg:\\s*([\\d\\s,]+);` 13 | ) 14 | 15 | const toRGB = (vals: string) => `rgb(${vals.replaceAll(' ', '')})` 16 | 17 | const extract = (match: RegExpExecArray | null) => { 18 | if (match && match.length > 2) { 19 | const accent = `[${toRGB(match[1])}]` 20 | const bg = `[${toRGB(match[2])}]` 21 | return { accent, bg } 22 | } 23 | return {} 24 | } 25 | 26 | export default function extractSchemeColors(colorScheme: string) { 27 | const scheme = colorScheme.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&') 28 | 29 | const css = fs.readFileSync(SCHEMES_FILE, 'utf-8') 30 | 31 | const light = extract(lightRegex(scheme).exec(css)) 32 | const dark = extract(darkRegex(scheme).exec(css)) 33 | 34 | return { 35 | light: { 36 | accent: 'black', 37 | bg: 'white', 38 | ...light 39 | }, 40 | dark: { 41 | accent: 'white', 42 | bg: 'black', 43 | ...dark 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/ogImages/index.ts: -------------------------------------------------------------------------------- 1 | import extractColorScheme from '@/ogImages/extractColorScheme' 2 | import post from '@/ogImages/post' 3 | import site from '@/ogImages/site' 4 | import config from '@/theme.config' 5 | import fs from 'fs' 6 | import satori, { type SatoriOptions } from 'satori' 7 | 8 | const loadFont = async (weight: string) => 9 | fs.readFileSync( 10 | `node_modules/@fontsource/ibm-plex-sans/files/ibm-plex-sans-latin-${weight}-normal.woff` 11 | ) 12 | 13 | const satoriOptions: SatoriOptions = { 14 | width: 1200, 15 | height: 630, 16 | embedFont: true, 17 | fonts: [ 18 | { 19 | name: 'IBM Plex Sans', 20 | data: await loadFont('400'), 21 | weight: 400, 22 | style: 'normal' 23 | }, 24 | { 25 | name: 'IBM Plex Sans', 26 | data: await loadFont('600'), 27 | weight: 600, 28 | style: 'normal' 29 | }, 30 | { 31 | name: 'IBM Plex Sans', 32 | data: await loadFont('700'), 33 | weight: 700, 34 | style: 'normal' 35 | } 36 | ] 37 | } 38 | 39 | const { mode, colorScheme } = config 40 | 41 | const { accent, bg } = extractColorScheme(colorScheme)[mode] 42 | 43 | const siteTemplate = site(accent, bg) 44 | const postTemplate = post(accent, bg) 45 | 46 | export default { 47 | site: (...args: Parameters) => 48 | satori(siteTemplate(...args), satoriOptions), 49 | post: (...args: Parameters) => 50 | satori(postTemplate(...args), satoriOptions) 51 | } 52 | -------------------------------------------------------------------------------- /src/ogImages/post.ts: -------------------------------------------------------------------------------- 1 | import config from '@/theme.config' 2 | import urlEncodedLogo from './urlEncodedLogo' 3 | 4 | const { title: siteTitle } = config 5 | 6 | export default (accent: string, bg: string) => 7 | (title: string, description: string, author: string) => ({ 8 | type: 'div', 9 | props: { 10 | tw: `flex flex-col w-full h-full p-8 bg-${bg} text-${accent}`, 11 | children: [ 12 | { 13 | type: 'div', 14 | props: { 15 | tw: 'flex', 16 | children: [ 17 | { 18 | type: 'img', 19 | props: { 20 | src: urlEncodedLogo, 21 | height: 48, 22 | width: 48 23 | } 24 | }, 25 | { 26 | type: 'span', 27 | props: { 28 | tw: 'ml-4 text-5xl font-bold', 29 | children: [siteTitle] 30 | } 31 | } 32 | ] 33 | } 34 | }, 35 | { 36 | type: 'div', 37 | props: { 38 | tw: 'flex flex-col w-full h-full justify-center items-center p-12', 39 | children: [ 40 | { 41 | type: 'div', 42 | props: { 43 | tw: `flex flex-col border-4 p-8 rounded-lg border-${accent}/50`, 44 | children: [ 45 | { 46 | type: 'span', 47 | props: { 48 | tw: 'text-7xl font-bold', 49 | children: [title] 50 | } 51 | }, 52 | { 53 | type: 'span', 54 | props: { 55 | tw: 'text-4xl mt-8 font-semibold', 56 | children: [description] 57 | } 58 | }, 59 | { 60 | type: 'div', 61 | props: { 62 | tw: 'flex justify-end text-3xl mt-8', 63 | children: [`by ${author}`] 64 | } 65 | } 66 | ] 67 | } 68 | } 69 | ] 70 | } 71 | } 72 | ] 73 | } 74 | }) 75 | -------------------------------------------------------------------------------- /src/ogImages/site.ts: -------------------------------------------------------------------------------- 1 | import config from '@/theme.config' 2 | import urlEncodedLogo from './urlEncodedLogo' 3 | 4 | const { title: siteTitle, description: siteDescription } = config 5 | 6 | export default (accent: string, bg: string) => () => ({ 7 | type: 'div', 8 | props: { 9 | tw: `flex flex-col w-full h-full p-8 bg-${bg} text-${accent}`, 10 | children: [ 11 | { 12 | type: 'div', 13 | props: { 14 | tw: 'flex flex-col w-full h-full justify-center items-center p-12', 15 | children: [ 16 | { 17 | type: 'div', 18 | props: { 19 | tw: `flex flex-col`, 20 | children: [ 21 | { 22 | type: 'div', 23 | props: { 24 | tw: 'flex', 25 | children: [ 26 | { 27 | type: 'img', 28 | props: { 29 | src: urlEncodedLogo, 30 | height: 128, 31 | width: 128 32 | } 33 | }, 34 | { 35 | type: 'span', 36 | props: { 37 | tw: 'ml-8 text-9xl font-bold', 38 | children: [siteTitle] 39 | } 40 | } 41 | ] 42 | } 43 | }, 44 | { 45 | type: 'span', 46 | props: { 47 | tw: 'text-6xl mt-8 font-semibold', 48 | children: [siteDescription] 49 | } 50 | } 51 | ] 52 | } 53 | } 54 | ] 55 | } 56 | } 57 | ] 58 | } 59 | }) 60 | -------------------------------------------------------------------------------- /src/ogImages/urlEncodedLogo.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | 3 | const LOGO_FILE = './src/assets/logo.svg' 4 | 5 | const svgString = fs.readFileSync(LOGO_FILE, 'utf-8') 6 | const encodedSVG = 'data:image/svg+xml,' + encodeURIComponent(svgString) 7 | 8 | export default encodedSVG 9 | -------------------------------------------------------------------------------- /src/pages/404.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: '@/layouts/LandingLayout.astro' 3 | title: 404 4 | description: Page not found 5 | background: true 6 | --- 7 | 8 | Not all those who wander are lost.
9 | But you are.\ 10 | Maybe you should [go back home](/). 11 | -------------------------------------------------------------------------------- /src/pages/about.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | layout: '@/layouts/PageLayout.astro' 3 | title: About 4 | activeHeaderLink: About 5 | --- 6 | 7 | # About 8 | 9 | Nordlys is a minimal Astro blog theme, ideal for a personal blog and showcasing a projects portfolio. It is fully customizable with baked-in theming and ready-to-use components. Developed by [FjellOverflow](https://github.com/FjellOverflow) using [Astro](https://astro.build/) and [Tailwindcss](https://tailwindcss.com/). The name _Nordlys_ comes from Norwegian and means [_Northern Lights_](https://en.wikipedia.org/wiki/Aurora) or [_Aurora_](https://en.wikipedia.org/wiki/Aurora), which is also reflected in the logo! 10 | 11 | import logo from '@/assets/logo.svg' 12 | import { Image } from 'astro:assets' 13 | 14 | Nordlys logo, a drawing of two gray mountains with green northern lights in the background 15 | 16 | ## Built-in Pages 17 | 18 | The theme includes a [landing](/) page, [blog](/posts/), [projects](/projects/) section, [about](/about/) page and a [404](/404/) page. Adding new content is straightforward - simply create a new `.md` file in the `src/pages`, `src/content/posts` or `src/content/projects` directories and start writing! Take a look at the provided examples to see how to tweak and configure the pages correctly. 19 | 20 | ## Configuration & Customization 21 | 22 | All basic configuration is done in `src/theme.config.ts`. To learn more about how you can configure and customize this theme, visit the [blog](/posts/) section. There you will find tutorials and feature showcases. Nordlys offers a variety of features, such as a light/dark mode, icons, custom components, color schemes, and headers for code-blocks. 23 | -------------------------------------------------------------------------------- /src/pages/authors/[author].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import PostsList from '@/components/PostsList.astro' 3 | import PageLayout, { 4 | type Props as PageLayoutProps 5 | } from '@/layouts/PageLayout.astro' 6 | import { generateAuthors, getPosts } from '@/util/posts' 7 | import type { InferGetStaticPropsType } from 'astro' 8 | 9 | export async function getStaticPaths() { 10 | const authors = await generateAuthors() 11 | 12 | return authors.map((author) => ({ 13 | params: { author }, 14 | props: { author } 15 | })) 16 | } 17 | 18 | const { author } = Astro.props 19 | 20 | type Props = InferGetStaticPropsType 21 | 22 | const posts = await getPosts(undefined, author) 23 | 24 | const frontmatter: PageLayoutProps['frontmatter'] = { 25 | title: `Author: ${author}`, 26 | activeHeaderLink: 'Blog' 27 | } 28 | --- 29 | 30 | 31 |

32 | 33 | {author} 34 |

35 | 36 |
37 | -------------------------------------------------------------------------------- /src/pages/feed.xml.ts: -------------------------------------------------------------------------------- 1 | import config from '@/theme.config' 2 | import { getPosts } from '@/util/posts' 3 | import rss from '@astrojs/rss' 4 | 5 | export async function GET() { 6 | const posts = await getPosts() 7 | 8 | return rss({ 9 | title: config.title, 10 | description: config.description, 11 | site: config.site, 12 | items: posts.map(({ data, id }) => ({ 13 | link: `posts/${id}/`, 14 | title: data.title, 15 | description: data.description, 16 | pubDate: new Date(data.publishedDate) 17 | })), 18 | customData: `${config.locale}` 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /src/pages/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: '@/layouts/LandingLayout.astro' 3 | title: Nordlys 4 | background: true 5 | logo: true 6 | --- 7 | 8 | A minimal Astro blog theme.\ 9 | Ideal for a personal [blog](/posts/) and showcasing a [projects](/projects/) portfolio.\ 10 | Fully customizable, dark mode & built-in color schemes.\ 11 | Read more in the [about](/about/) section. 12 | -------------------------------------------------------------------------------- /src/pages/ogImage.png.ts: -------------------------------------------------------------------------------- 1 | import ogImages from '@/ogImages' 2 | import config from '@/theme.config' 3 | import { Resvg } from '@resvg/resvg-js' 4 | import type { APIRoute } from 'astro' 5 | 6 | export const GET: APIRoute = async () => { 7 | if (config.openGraphImage) return new Response() 8 | 9 | const svg = await ogImages.site() 10 | const png = new Resvg(svg).render().asPng() 11 | 12 | return new Response(png, { 13 | headers: { 'Content-Type': 'image/png' } 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /src/pages/posts/[...id].png.ts: -------------------------------------------------------------------------------- 1 | import ogImages from '@/ogImages' 2 | import { getPosts } from '@/util/posts' 3 | import { Resvg } from '@resvg/resvg-js' 4 | import type { APIContext, APIRoute, InferGetStaticPropsType } from 'astro' 5 | 6 | export async function getStaticPaths() { 7 | const posts = (await getPosts()).filter((p) => !p.data.openGraphImage) 8 | 9 | return posts.map((post) => ({ 10 | params: { id: post.id }, 11 | props: { post } 12 | })) 13 | } 14 | 15 | export const GET: APIRoute = async ({ props }: APIContext) => { 16 | const { post } = props as InferGetStaticPropsType 17 | const { title, description, author } = post.data 18 | 19 | const svg = await ogImages.post(title, description, author) 20 | const png = new Resvg(svg).render().asPng() 21 | 22 | return new Response(png, { 23 | headers: { 'Content-Type': 'image/png' } 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /src/pages/posts/[...id]/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import PostLayout from '@/layouts/PostLayout.astro' 3 | import { getPosts } from '@/util/posts' 4 | import type { InferGetStaticPropsType } from 'astro' 5 | 6 | const { post } = Astro.props 7 | 8 | type Props = InferGetStaticPropsType 9 | 10 | export async function getStaticPaths() { 11 | const posts = await getPosts() 12 | 13 | return posts.map((post) => ({ 14 | params: { id: post.id }, 15 | props: { post } 16 | })) 17 | } 18 | --- 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/pages/posts/[...page].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Pagination from '@/components/Pagination.astro' 3 | import PostsList from '@/components/PostsList.astro' 4 | import PageLayout, { 5 | type Props as PageLayoutProps 6 | } from '@/layouts/PageLayout.astro' 7 | import config from '@/theme.config' 8 | import { getPosts } from '@/util/posts' 9 | import type { GetStaticPaths, InferGetStaticPropsType } from 'astro' 10 | 11 | export const getStaticPaths = (async ({ paginate }) => { 12 | const posts = await getPosts() 13 | 14 | return paginate(posts, { pageSize: config.postsPerPage }) 15 | }) satisfies GetStaticPaths 16 | 17 | type Props = InferGetStaticPropsType 18 | 19 | const { page } = Astro.props 20 | 21 | const frontmatter: PageLayoutProps['frontmatter'] = { 22 | title: 'Posts', 23 | activeHeaderLink: 'Blog', 24 | canonicalURL: new URL('/posts/1/', Astro.site).toString() 25 | } 26 | --- 27 | 28 | 29 |

30 | {frontmatter.title} 31 |

32 | 33 | 34 | 35 |
36 | -------------------------------------------------------------------------------- /src/pages/posts/[page].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Pagination from '@/components/Pagination.astro' 3 | import PostsList from '@/components/PostsList.astro' 4 | import PageLayout, { 5 | type Props as PageLayoutProps 6 | } from '@/layouts/PageLayout.astro' 7 | import config from '@/theme.config' 8 | import { getPosts } from '@/util/posts' 9 | import type { GetStaticPaths, InferGetStaticPropsType } from 'astro' 10 | 11 | export const getStaticPaths = (async ({ paginate }) => { 12 | const posts = await getPosts() 13 | 14 | return paginate(posts, { pageSize: config.postsPerPage }) 15 | }) satisfies GetStaticPaths 16 | 17 | type Props = InferGetStaticPropsType 18 | 19 | const { page } = Astro.props 20 | 21 | const frontmatter: PageLayoutProps['frontmatter'] = { 22 | title: 'Posts', 23 | activeHeaderLink: 'Blog' 24 | } 25 | --- 26 | 27 | 28 |

29 | {frontmatter.title} 30 | {page.currentPage > 1 && {` (page ${page.currentPage})`}} 31 |

32 | 33 | 34 | 35 |
36 | -------------------------------------------------------------------------------- /src/pages/projects/[...page].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Pagination from '@/components/Pagination.astro' 3 | import ProjectsList from '@/components/ProjectsList.astro' 4 | import PageLayout, { 5 | type Props as PageLayoutProps 6 | } from '@/layouts/PageLayout.astro' 7 | import config from '@/theme.config' 8 | import { getProjects } from '@/util/projects' 9 | import type { GetStaticPaths, InferGetStaticPropsType } from 'astro' 10 | 11 | export const getStaticPaths = (async ({ paginate }) => { 12 | const projects = await getProjects() 13 | 14 | return paginate(projects, { pageSize: config.projectsPerPage }) 15 | }) satisfies GetStaticPaths 16 | 17 | type Props = InferGetStaticPropsType 18 | 19 | const { page } = Astro.props 20 | 21 | const frontmatter: PageLayoutProps['frontmatter'] = { 22 | title: 'Projects', 23 | activeHeaderLink: 'Projects', 24 | canonicalURL: new URL('/projects/1', Astro.site).toString() 25 | } 26 | --- 27 | 28 | 29 |

30 | {frontmatter.title} 31 |

32 | 33 | 34 | 35 |
36 | -------------------------------------------------------------------------------- /src/pages/projects/[page].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Pagination from '@/components/Pagination.astro' 3 | import ProjectsList from '@/components/ProjectsList.astro' 4 | import PageLayout, { 5 | type Props as PageLayoutProps 6 | } from '@/layouts/PageLayout.astro' 7 | import config from '@/theme.config' 8 | import { getProjects } from '@/util/projects' 9 | import type { GetStaticPaths, InferGetStaticPropsType } from 'astro' 10 | 11 | export const getStaticPaths = (async ({ paginate }) => { 12 | const projects = await getProjects() 13 | 14 | return paginate(projects, { pageSize: config.projectsPerPage }) 15 | }) satisfies GetStaticPaths 16 | 17 | type Props = InferGetStaticPropsType 18 | 19 | const { page } = Astro.props 20 | 21 | const frontmatter: PageLayoutProps['frontmatter'] = { 22 | title: 'Projects', 23 | activeHeaderLink: 'Projects' 24 | } 25 | --- 26 | 27 | 28 |

29 | {frontmatter.title} 30 | {page.currentPage > 1 && {` (page ${page.currentPage})`}} 31 |

32 | 33 | 34 | 35 |
36 | -------------------------------------------------------------------------------- /src/pages/robots.txt.ts: -------------------------------------------------------------------------------- 1 | import type { APIRoute } from 'astro' 2 | 3 | const getRobotsTxt = (sitemapURL: URL) => ` 4 | User-agent: * 5 | Allow: / 6 | 7 | Sitemap: ${sitemapURL.href} 8 | ` 9 | 10 | export const GET: APIRoute = ({ site }) => { 11 | const sitemapURL = new URL('sitemap-index.xml', site) 12 | return new Response(getRobotsTxt(sitemapURL)) 13 | } 14 | -------------------------------------------------------------------------------- /src/pages/search.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import PageLayout, { 3 | type Props as PageLayoutProps 4 | } from '@/layouts/PageLayout.astro' 5 | import PagefindPlugin from '@/plugins/PagefindPlugin.astro' 6 | 7 | const frontmatter: PageLayoutProps['frontmatter'] = { 8 | title: 'Search', 9 | activeHeaderLink: '/search' 10 | } 11 | --- 12 | 13 | 14 |

{frontmatter.title}

15 | 16 | 21 | 22 | 23 |

Type to search

24 |
    25 | 26 | 27 |
    28 | 29 | 100 | -------------------------------------------------------------------------------- /src/pages/tags/[tag].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import PostsList from '@/components/PostsList.astro' 3 | import ProjectsList from '@/components/ProjectsList.astro' 4 | import PageLayout, { 5 | type Props as PageLayoutProps 6 | } from '@/layouts/PageLayout.astro' 7 | import { getPosts } from '@/util/posts' 8 | import { getProjects } from '@/util/projects' 9 | import { generateTags } from '@/util/tags' 10 | import type { InferGetStaticPropsType } from 'astro' 11 | 12 | const { 13 | tag: { tag, icon } 14 | } = Astro.props 15 | 16 | type Props = InferGetStaticPropsType 17 | 18 | export async function getStaticPaths() { 19 | const tags = await generateTags() 20 | 21 | return tags.map(({ tag, icon }) => ({ 22 | params: { tag }, 23 | props: { tag: { tag, icon } } 24 | })) 25 | } 26 | 27 | const posts = await getPosts(tag) 28 | 29 | const projects = await getProjects(tag) 30 | 31 | const frontmatter: PageLayoutProps['frontmatter'] = { 32 | title: `Tag: ${tag}`, 33 | activeHeaderLink: 'Tags' 34 | } 35 | --- 36 | 37 | 38 | 44 | 45 |

    46 | 47 | {tag} 48 |

    49 | {!!posts.length &&

    Posts

    } 50 | 51 | {!!projects.length &&

    Projects

    } 52 | 53 |
    54 | -------------------------------------------------------------------------------- /src/pages/tags/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import PageLayout, { 3 | type Props as PageLayoutProps 4 | } from '@/layouts/PageLayout.astro' 5 | import { generateTags, getTagUsage } from '@/util/tags' 6 | 7 | const tags = await generateTags() 8 | 9 | const frontmatter: PageLayoutProps['frontmatter'] = { 10 | title: 'Tags', 11 | activeHeaderLink: 'Tags' 12 | } 13 | --- 14 | 15 | 16 |

    Tags

    17 |
    18 | { 19 | tags.map(({ tag, icon }) => ( 20 | 25 | 26 | 27 | 28 | {tag} ({getTagUsage(tag)}) 29 | 30 | 31 | )) 32 | } 33 |
    34 |
    35 | -------------------------------------------------------------------------------- /src/plugins/CopyCodeButtonsPlugin.astro: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | --- 4 | 5 | 27 | -------------------------------------------------------------------------------- /src/plugins/HeadingAnchorsPlugin.astro: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | --- 4 | 5 | 23 | -------------------------------------------------------------------------------- /src/plugins/PagefindPlugin.astro: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /src/plugins/ScrollProgressPlugin.astro: -------------------------------------------------------------------------------- 1 | 2 |
    3 |
    4 | 5 | 31 | -------------------------------------------------------------------------------- /src/plugins/ScrollToTopPlugin.astro: -------------------------------------------------------------------------------- 1 | 12 | 13 | 43 | -------------------------------------------------------------------------------- /src/plugins/codeHeadersPlugin.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import type { Icon } from '@/types' 3 | 4 | const iconMap: Record = { 5 | plaintext: 'tabler--dots', 6 | 7 | 'angular-html': 'tabler--brand-angular', 8 | 'angular-ts': 'tabler--brand-angular', 9 | astro: 'tabler--brand-astro', 10 | cpp: 'tabler--brand-cpp', 11 | csharp: 'tabler--brand-c-sharp', 12 | cs: 'tabler--brand-c-sharp', 13 | css: 'tabler--brand-css3', 14 | csv: 'tabler--csv', 15 | diff: 'tabler--file-diff', 16 | docker: 'tabler--brand-docker', 17 | dockerfile: 'tabler--brand-docker', 18 | go: 'tabler--brand-golang', 19 | graphql: 'tabler--brand-graphql', 20 | gql: 'tabler--brand-graphql', 21 | html: 'tabler--brand-html5', 22 | javascript: 'tabler--brand-javascript', 23 | js: 'tabler--brand-javascript', 24 | json: 'tabler--json', 25 | jsonc: 'tabler--json', 26 | jsx: 'tabler--brand-react', 27 | kotlin: 'tabler--brand-kotlin', 28 | kt: 'tabler--brand-kotlin', 29 | kts: 'tabler--brand-kotlin', 30 | markdown: 'tabler--markdown', 31 | md: 'tabler--markdown', 32 | mdx: 'tabler--markdown', 33 | php: 'tabler--brand-php', 34 | powershell: 'tabler--brand-powershell', 35 | ps: 'tabler--brand-powershell', 36 | ps1: 'tabler--brand-powershell', 37 | python: 'tabler--brand-python', 38 | py: 'tabler--brand-python', 39 | rust: 'tabler--brand-rust', 40 | rs: 'tabler--brand-rust', 41 | sass: 'tabler--brand-sass', 42 | shellscript: 'tabler--terminal', 43 | bash: 'tabler--terminal', 44 | sh: 'tabler--terminal', 45 | shell: 'tabler--terminal', 46 | zsh: 'tabler--terminal', 47 | sql: 'tabler--sql', 48 | svelte: 'tabler--brand-svelte', 49 | swift: 'tabler--brand-swift', 50 | tsx: 'tabler--brand-typescript', 51 | typescript: 'tabler--brand-typescript', 52 | ts: 'tabler--brand-typescript', 53 | vue: 'tabler--brand-vue', 54 | 'vue-html': 'tabler--brand-vue', 55 | xml: 'tabler--file-type-xml' 56 | } 57 | 58 | function generateHeaderTitle(label: string) { 59 | return { 60 | type: 'element', 61 | tagName: 'code', 62 | properties: { className: ['label'] }, 63 | children: [{ type: 'text', value: label }] 64 | } 65 | } 66 | 67 | function generateHeaderIcon(lang: string) { 68 | const icon = iconMap[lang || 'plaintext'] || iconMap['plaintext'] 69 | const textSize = icon === iconMap['plaintext'] ? 'text-5xl' : 'text-2xl' 70 | 71 | return { 72 | type: 'element', 73 | tagName: 'span', 74 | properties: { className: ['lang-icon', 'iconify', icon, textSize] }, 75 | children: [] 76 | } 77 | } 78 | 79 | const copyCodeBtn = { 80 | type: 'element', 81 | tagName: 'button', 82 | properties: { 83 | title: 'Copy code', 84 | 'aria-label': 'Copy code', 85 | className: ['flex', 'items-center'] 86 | }, 87 | children: [ 88 | { 89 | type: 'element', 90 | tagName: 'span', 91 | properties: { 92 | 'aria-hidden': 'true', 93 | className: ['copy-btn'] 94 | } 95 | } 96 | ] 97 | } 98 | 99 | const preTransformer = (label?: string, lang?: string) => (ast: any) => { 100 | const styles = parseStyleProps(ast.properties.style) 101 | 102 | const color = styles['color'] 103 | const colorDark = styles['--shiki-dark'] 104 | const bg = styles['background-color'] 105 | const bgDark = styles['--shiki-dark-bg'] 106 | 107 | const codeHeader = { 108 | type: 'element', 109 | tagName: 'div', 110 | properties: { className: ['flex', 'gap-2'] }, 111 | children: [] as Array 112 | } 113 | 114 | if (lang) codeHeader.children.push(generateHeaderIcon(lang)) 115 | if (label) codeHeader.children.push(generateHeaderTitle(label)) 116 | 117 | return { 118 | type: 'element', 119 | tagName: 'div', 120 | properties: { 121 | style: `--code-header-color:${color};--code-header-color-dark:${colorDark};--code-header-bg:${bg};--code-header-bg-dark:${bgDark}` 122 | }, 123 | children: [ 124 | { 125 | type: 'element', 126 | tagName: 'div', 127 | properties: { 128 | class: `astro-code-header` 129 | }, 130 | children: [codeHeader, copyCodeBtn] 131 | }, 132 | ast 133 | ] 134 | } 135 | } 136 | 137 | export default { 138 | preprocess: (_raw: string, options: any) => { 139 | const pre = preTransformer(options.meta?.__raw, options.lang) 140 | options.transformers[1].pre = pre 141 | } 142 | } 143 | 144 | function parseStyleProps(style: string): Record { 145 | const propArr = style.split(';') 146 | 147 | const propMap: Record = {} 148 | 149 | propArr.forEach((prop) => { 150 | const [key, val] = prop.split(':') 151 | propMap[key] = val 152 | }) 153 | 154 | return propMap 155 | } 156 | -------------------------------------------------------------------------------- /src/plugins/readingTimePlugin.ts: -------------------------------------------------------------------------------- 1 | import { toString } from 'mdast-util-to-string' 2 | import getReadingTime from 'reading-time' 3 | 4 | export default function () { 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 | return function (tree: any, { data }: any) { 7 | const textOnPage = toString(tree) 8 | const readingTime = getReadingTime(textOnPage) 9 | data.astro.frontmatter.readingTime = readingTime.text 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/style/astro-code.css: -------------------------------------------------------------------------------- 1 | @utility astro-code-header { 2 | @apply border-accent flex h-12 items-center justify-between rounded-tl-md rounded-tr-md border bg-(--code-header-bg) px-4 text-(--code-header-color) dark:bg-(--code-header-bg-dark) dark:text-(--code-header-color-dark); 3 | 4 | .label { 5 | display: flex; 6 | flex-direction: column; 7 | justify-content: center; 8 | margin-top: 0.08rem; 9 | font-size: var(--text-sm); 10 | color: var(--code-header-color); 11 | 12 | html[data-mode='dark'] & { 13 | color: var(--code-header-color-dark); 14 | } 15 | } 16 | 17 | .copy-btn { 18 | @apply iconify tabler--copy cursor-pointer text-2xl; 19 | } 20 | 21 | .copy-success { 22 | @apply iconify tabler--check text-2xl; 23 | } 24 | } 25 | 26 | .astro-code { 27 | margin-top: 0; 28 | border-color: rgb(var(--accent)); 29 | border-top-left-radius: 0; 30 | border-top-right-radius: 0; 31 | border-width: 1px; 32 | border-top-width: 0; 33 | } 34 | 35 | html[data-mode='dark'] { 36 | .astro-code, 37 | .astro-code span { 38 | color: var(--shiki-dark) !important; 39 | background-color: var(--shiki-dark-bg) !important; 40 | font-style: var(--shiki-dark-font-style) !important; 41 | font-weight: var(--shiki-dark-font-weight) !important; 42 | text-decoration: var(--shiki-dark-text-decoration) !important; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/style/color-schemes.css: -------------------------------------------------------------------------------- 1 | @layer base { 2 | .scheme-mono { 3 | --accent: 0, 0, 0; 4 | --accent-bg: 255, 255, 255; 5 | 6 | &[data-mode='dark'] { 7 | --accent: 255, 255, 255; 8 | --accent-bg: 0, 0, 0; 9 | } 10 | } 11 | 12 | .scheme-nord { 13 | --accent: 94, 129, 172; 14 | --accent-bg: 242, 244, 248; 15 | 16 | &[data-mode='dark'] { 17 | --accent: 136, 192, 208; 18 | --accent-bg: 36, 41, 51; 19 | } 20 | } 21 | 22 | .scheme-aurora { 23 | --accent: 0, 183, 86; 24 | --accent-bg: 242, 244, 248; 25 | 26 | &[data-mode='dark'] { 27 | --accent: 0, 253, 119; 28 | --accent-bg: 35, 39, 48; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/style/main.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | 3 | @import './color-schemes.css' layer(base); 4 | @import './astro-code.css'; 5 | @reference './theme.css'; 6 | 7 | @custom-variant dark (&:where([data-mode=dark], [data-mode=dark] *)); 8 | 9 | @layer base { 10 | :root { 11 | @apply bg-accent-bg selection:bg-accent/75 overflow-y-scroll scroll-smooth font-sans underline-offset-4 selection:text-white dark:selection:text-black; 12 | 13 | img[data-img-embed=''], 14 | img[data-img-embed='true'] { 15 | border-color: rgb(var(--accent)); 16 | border-radius: var(--radius-sm); 17 | border-width: 1px; 18 | } 19 | 20 | .zoom-overlay { 21 | background-color: #000000bf !important; 22 | } 23 | } 24 | } 25 | 26 | @utility clickable { 27 | @apply hover:text-accent cursor-pointer opacity-75 transition duration-500 ease-in-out hover:opacity-100; 28 | } 29 | -------------------------------------------------------------------------------- /src/style/theme.css: -------------------------------------------------------------------------------- 1 | @theme inline { 2 | --font-sans: 'IBM Plex Sans', ui-sans-serif; 3 | 4 | --color-accent: rgb(var(--accent)); 5 | --color-accent-bg: rgb(var(--accent-bg)); 6 | } 7 | 8 | @plugin '@tailwindcss/typography'; 9 | 10 | @plugin 'tailwind-scrollbar-hide'; 11 | 12 | @plugin '@iconify/tailwind4' { 13 | prefixes: tabler; 14 | } 15 | -------------------------------------------------------------------------------- /src/theme.config.ts: -------------------------------------------------------------------------------- 1 | import { defineThemeConfig } from './types' 2 | 3 | export default defineThemeConfig({ 4 | site: 'https://nordlys.fjelloverflow.dev', 5 | title: 'Nordlys', 6 | description: 'A minimal Astro blog theme', 7 | author: 'FjellOverflow', 8 | navbarItems: [ 9 | { label: 'Blog', href: '/posts/' }, 10 | { label: 'Projects', href: '/projects/' }, 11 | { label: 'Tags', href: '/tags/' }, 12 | { label: 'About', href: '/about/' }, 13 | { 14 | label: 'Other pages', 15 | children: [ 16 | { label: 'Landing page', href: '/' }, 17 | { label: '404 page', href: '/404' }, 18 | { label: 'Author: FjellOverflow', href: '/authors/FjellOverflow/' }, 19 | { label: 'Tag: documentation', href: '/tags/documentation/' } 20 | ] 21 | } 22 | ], 23 | footerItems: [ 24 | { 25 | icon: 'tabler--brand-github', 26 | href: 'https://github.com/FjellOverflow/nordlys', 27 | label: 'Github' 28 | }, 29 | { 30 | icon: 'tabler--rss', 31 | href: '/feed.xml', 32 | label: 'RSS feed' 33 | } 34 | ], 35 | 36 | // optional settings 37 | locale: 'en', 38 | mode: 'dark', 39 | modeToggle: true, 40 | colorScheme: 'scheme-mono', 41 | openGraphImage: undefined, 42 | postsPerPage: 4, 43 | projectsPerPage: 3, 44 | scrollProgress: false, 45 | scrollToTop: true, 46 | tagIcons: { 47 | tailwindcss: 'tabler--brand-tailwind', 48 | astro: 'tabler--brand-astro', 49 | documentation: 'tabler--book' 50 | }, 51 | shikiThemes: { 52 | light: 'vitesse-light', 53 | dark: 'vitesse-black' 54 | } 55 | }) 56 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { ShikiConfig } from 'astro' 2 | import type { SetOptional } from 'type-fest' 3 | 4 | export type Icon = `tabler--${string}` 5 | 6 | export interface ResolvedTag { 7 | tag: string 8 | icon: Icon 9 | } 10 | 11 | export interface NavItem { 12 | label: string 13 | href: string 14 | icon?: Icon 15 | } 16 | 17 | export interface NavItemParent { 18 | label: string 19 | icon?: Icon 20 | children: NavItem[] 21 | } 22 | 23 | export type HeaderItem = NavItem | NavItemParent 24 | 25 | const Modes = ['dark', 'light'] as const 26 | 27 | export const ColorSchemes = [ 28 | 'scheme-mono', 29 | 'scheme-nord', 30 | 'scheme-aurora' 31 | ] as const 32 | 33 | export type Mode = (typeof Modes)[number] 34 | export type ColorScheme = (typeof ColorSchemes)[number] 35 | 36 | export interface ThemeConfig { 37 | site: string 38 | title: string 39 | description: string 40 | author: string 41 | navbarItems: HeaderItem[] 42 | footerItems: NavItem[] 43 | 44 | locale: string 45 | mode: Mode 46 | modeToggle: boolean 47 | colorScheme: ColorScheme 48 | openGraphImage: ImageMetadata | string | undefined 49 | postsPerPage: number 50 | projectsPerPage: number 51 | scrollProgress: boolean 52 | scrollToTop: boolean 53 | tagIcons: Record 54 | shikiThemes: ShikiConfig['themes'] 55 | } 56 | 57 | const defaults = { 58 | locale: 'en', 59 | mode: Modes[0], 60 | modeToggle: true, 61 | colorScheme: ColorSchemes[0], 62 | openGraphImage: undefined, 63 | postsPerPage: 4, 64 | projectsPerPage: 3, 65 | scrollProgress: false, 66 | scrollToTop: true, 67 | tagIcons: {}, 68 | shikiThemes: { 69 | light: 'vitesse-light', 70 | dark: 'vitesse-black' 71 | } as ShikiConfig['themes'] 72 | } 73 | 74 | type PartialThemeConfig = SetOptional 75 | 76 | export const defineThemeConfig = (config: PartialThemeConfig): ThemeConfig => { 77 | return { 78 | ...defaults, 79 | ...config 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/util/index.ts: -------------------------------------------------------------------------------- 1 | export const toDateString = (date: Date): string => 2 | date.toLocaleDateString('en-US', { 3 | year: 'numeric', 4 | month: 'short', 5 | day: 'numeric' 6 | }) 7 | 8 | export const toMonthString = (date: Date): string => 9 | date.toLocaleDateString('en-US', { 10 | year: 'numeric', 11 | month: 'long', 12 | day: undefined 13 | }) 14 | 15 | export const isLocalAsset = ( 16 | image: string | ImageMetadata 17 | ): image is ImageMetadata => typeof image !== 'string' 18 | 19 | export const resolveImageUrl = (image: string | ImageMetadata) => { 20 | return isLocalAsset(image) ? image.src : image 21 | } 22 | 23 | export const isAbsolute = (url: string) => 24 | url.indexOf('http://') === 0 || url.indexOf('https://') === 0 25 | 26 | export const generateItemId = (base: string) => 27 | (base + Math.random().toString(16).slice(2)).replaceAll(' ', '').toLowerCase() 28 | -------------------------------------------------------------------------------- /src/util/posts.ts: -------------------------------------------------------------------------------- 1 | import { getCollection, type CollectionEntry } from 'astro:content' 2 | 3 | export const sortPosts = ( 4 | p1: CollectionEntry<'posts'>, 5 | p2: CollectionEntry<'posts'> 6 | ) => p2.data.publishedDate.getTime() - p1.data.publishedDate.getTime() 7 | 8 | export const getPosts = async ( 9 | tag?: string, 10 | author?: string, 11 | includeDrafts = import.meta.env.DEV 12 | ) => { 13 | const posts = await getCollection('posts') 14 | 15 | posts.sort(sortPosts) 16 | 17 | return posts 18 | .filter( 19 | (p) => 20 | !tag || p.data.tags.some((t) => t.toLowerCase() === tag.toLowerCase()) 21 | ) 22 | .filter( 23 | (p) => !author || p.data.author.toLowerCase() === author.toLowerCase() 24 | ) 25 | .filter((p) => includeDrafts || !p.data.draft) 26 | } 27 | 28 | export const adjacentPosts = async (post: CollectionEntry<'posts'>) => { 29 | const reversedPosts = (await getPosts()).reverse() 30 | const postIndex = reversedPosts.findIndex((p) => p.id === post.id) 31 | 32 | if (postIndex < 0) return {} 33 | 34 | return { 35 | previous: reversedPosts[postIndex - 1], 36 | next: reversedPosts[postIndex + 1] 37 | } 38 | } 39 | 40 | export const generateAuthors = async () => { 41 | const posts = await getPosts() 42 | 43 | return [...new Set(posts.map((p) => p.data.author))] 44 | } 45 | -------------------------------------------------------------------------------- /src/util/projects.ts: -------------------------------------------------------------------------------- 1 | import { getCollection, type CollectionEntry } from 'astro:content' 2 | 3 | export const sortProjects = ( 4 | p1: CollectionEntry<'projects'>, 5 | p2: CollectionEntry<'projects'> 6 | ) => { 7 | if (!p1.data.startDate && !p2.data.startDate) 8 | return p1.data.title.localeCompare(p2.data.title) 9 | 10 | if (!p1.data.startDate && p2.data.startDate) return 1 11 | 12 | if (p1.data.startDate && !p2.data.startDate) return -1 13 | 14 | const endDateDiff = 15 | (p2.data.endDate?.getTime() || Number.MAX_SAFE_INTEGER) - 16 | (p1.data.endDate?.getTime() || Number.MAX_SAFE_INTEGER) 17 | const startDateDiff = 18 | (p2.data.startDate?.getTime() || 0) - (p1.data.startDate?.getTime() || 0) 19 | 20 | return ( 21 | endDateDiff || startDateDiff || p1.data.title.localeCompare(p2.data.title) 22 | ) 23 | } 24 | 25 | export const getProjects = async (tag?: string) => { 26 | const projects = await getCollection('projects') 27 | 28 | projects.sort(sortProjects) 29 | 30 | return projects.filter( 31 | (p) => 32 | !tag || p.data.tags.some((t) => t.toLowerCase() === tag.toLowerCase()) 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /src/util/tags.ts: -------------------------------------------------------------------------------- 1 | import config from '@/theme.config' 2 | import type { ResolvedTag } from '@/types' 3 | import { getPosts } from '@/util/posts' 4 | import { getProjects } from '@/util/projects' 5 | 6 | export const sortTags = (t1: ResolvedTag, t2: ResolvedTag) => 7 | t1.tag.localeCompare(t2.tag) 8 | 9 | export const resolveTags = (rawTags: string[]): ResolvedTag[] => { 10 | const resolvedTags = [...new Set(rawTags)].map((t) => { 11 | const tag = t.toLowerCase() 12 | 13 | return { 14 | tag, 15 | icon: config.tagIcons[tag] || 'tabler--tag' 16 | } 17 | }) 18 | 19 | resolvedTags.sort(sortTags) 20 | 21 | return resolvedTags 22 | } 23 | 24 | export const generateTags = async (): Promise => { 25 | const allTags = [...(await getPosts()), ...(await getProjects())].flatMap( 26 | (p) => p.data.tags 27 | ) 28 | 29 | return resolveTags([...new Set(allTags)]) 30 | } 31 | 32 | export const getTagUsage = async (tag: string): Promise => 33 | (await getPosts(tag)).length + (await getProjects(tag)).length 34 | -------------------------------------------------------------------------------- /stylelint.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('stylelint').Config} */ 2 | export default { 3 | extends: ['stylelint-config-recommended', 'stylelint-config-html'], 4 | rules: { 5 | 'at-rule-no-unknown': null, 6 | 'at-rule-no-deprecated': null 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "include": [ 4 | ".astro/types.d.ts", 5 | "**/*" 6 | ], 7 | "compilerOptions": { 8 | "baseUrl": ".", 9 | "paths": { 10 | "@/*": [ 11 | "src/*" 12 | ] 13 | } 14 | }, 15 | "exclude": [ 16 | "dist" 17 | ] 18 | } --------------------------------------------------------------------------------