├── .gitignore ├── www ├── static │ ├── icon.png │ └── pyro_bg.png ├── pages │ ├── guides │ │ ├── index.md │ │ └── deployment │ │ │ ├── index.md │ │ │ ├── deno-deploy.md │ │ │ └── github-pages.md │ ├── _hidden.md │ ├── core-concepts │ │ ├── index.md │ │ ├── plugins.md │ │ ├── pages.md │ │ ├── sidebar.md │ │ ├── unocss.md │ │ └── markdown-features.md │ ├── getting-started │ │ ├── index.md │ │ ├── configuration.md │ │ └── installation.md │ └── index.tsx └── pyro.yml ├── tests └── end_to_end │ ├── deno_json │ ├── pyro.yml │ ├── static │ │ └── icon.png │ ├── deno.jsonc │ └── pages │ │ └── index.tsx │ ├── unocss │ ├── pyro.yml │ ├── static │ │ └── icon.png │ ├── pages │ │ └── index.md │ └── uno.config.ts │ └── json_config │ ├── pyro.json │ ├── static │ └── icon.png │ └── pages │ ├── getting-started │ ├── index.md │ └── submenu.md │ └── index.md ├── page.ts ├── plugins └── demo.tsx ├── deno.jsonc ├── README.md ├── .github └── workflows │ ├── ci.yml │ └── deploy.yml ├── cli.ts ├── install.ts ├── LICENSE ├── src ├── lib │ ├── types.ts │ ├── magic.ts │ ├── route_map.ts │ ├── rehype_starry_night.ts │ ├── sidebar.tsx │ ├── render.ts │ ├── page.tsx │ └── css.ts ├── generate.ts ├── dev.ts ├── build.ts └── utils.tsx └── deps.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | build 3 | deno.lock -------------------------------------------------------------------------------- /www/static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lino-levan/pyro/HEAD/www/static/icon.png -------------------------------------------------------------------------------- /tests/end_to_end/deno_json/pyro.yml: -------------------------------------------------------------------------------- 1 | title: Pyro Site 2 | github: https://github.com/lino-levan/pyro -------------------------------------------------------------------------------- /tests/end_to_end/unocss/pyro.yml: -------------------------------------------------------------------------------- 1 | title: Pyro Site 2 | github: https://github.com/lino-levan/pyro -------------------------------------------------------------------------------- /www/static/pyro_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lino-levan/pyro/HEAD/www/static/pyro_bg.png -------------------------------------------------------------------------------- /tests/end_to_end/json_config/pyro.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Pyro Site", 3 | "github": "https://github.com/lino-levan/pyro" 4 | } 5 | -------------------------------------------------------------------------------- /tests/end_to_end/unocss/static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lino-levan/pyro/HEAD/tests/end_to_end/unocss/static/icon.png -------------------------------------------------------------------------------- /tests/end_to_end/deno_json/static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lino-levan/pyro/HEAD/tests/end_to_end/deno_json/static/icon.png -------------------------------------------------------------------------------- /tests/end_to_end/json_config/static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lino-levan/pyro/HEAD/tests/end_to_end/json_config/static/icon.png -------------------------------------------------------------------------------- /tests/end_to_end/deno_json/deno.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "icons/": "https://deno.land/x/tabler_icons_tsx@0.0.4/tsx/" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /page.ts: -------------------------------------------------------------------------------- 1 | import { JSX } from "./src/lib/types.ts"; 2 | 3 | export type PageProps = { 4 | header: JSX.Element; 5 | footer?: JSX.Element; 6 | }; 7 | -------------------------------------------------------------------------------- /tests/end_to_end/json_config/pages/getting-started/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | description: My first Pyro directory 4 | index: 1 5 | --- 6 | -------------------------------------------------------------------------------- /tests/end_to_end/json_config/pages/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hello World 3 | description: My first Pyro page 4 | index: 0 5 | --- 6 | 7 | How are you doing today? 8 | -------------------------------------------------------------------------------- /www/pages/guides/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Guides 3 | description: Pyro was designed from the ground up to be no-config and incredibly fast. 4 | index: 3 5 | --- 6 | 7 | Let's go through actually using Pyro in the real world! 8 | -------------------------------------------------------------------------------- /www/pages/_hidden.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hidden 3 | --- 4 | 5 | This is a hidden page. It doesn't get rendered in the sidebar and it doesn't get 6 | routed to anything. It is only here for imports / exports / notes to 7 | maintainers. 8 | -------------------------------------------------------------------------------- /www/pages/core-concepts/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Core concepts 3 | description: Pyro was designed from the ground up to be no-config and incredibly fast. 4 | index: 2 5 | --- 6 | 7 | Let's learn about the most important Pyro concepts! 8 | -------------------------------------------------------------------------------- /www/pages/guides/deployment/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Deployment 3 | description: Pyro was designed from the ground up to be no-config and incredibly fast. 4 | index: 0 5 | --- 6 | 7 | Let's go through deploying Pyro in the real world! 8 | -------------------------------------------------------------------------------- /tests/end_to_end/unocss/pages/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hello World 3 | description: My first Pyro page 4 | index: 0 5 | --- 6 | 7 | How are you doing today? 8 | 9 |
10 |

hi

11 |

this would only render as big if prose worked

12 |
13 | -------------------------------------------------------------------------------- /tests/end_to_end/deno_json/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import IconArrowRight from "icons/arrow-right.tsx"; 2 | 3 | export const config = { 4 | title: "Hello World", 5 | description: "My first Pyro page", 6 | }; 7 | 8 | export default function Page() { 9 | return ( 10 |
11 | hi 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /tests/end_to_end/unocss/uno.config.ts: -------------------------------------------------------------------------------- 1 | import { presetUno } from "https://esm.sh/@unocss/preset-uno@0.58.0"; 2 | import { presetTypography } from "https://esm.sh/@unocss/preset-typography@0.58.0"; 3 | 4 | export default { 5 | presets: [ 6 | presetUno({ 7 | dark: "media", 8 | }), 9 | presetTypography(), 10 | ], 11 | }; 12 | -------------------------------------------------------------------------------- /tests/end_to_end/json_config/pages/getting-started/submenu.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Submenu 3 | description: My first Pyro directory 4 | index: 0 5 | --- 6 | 7 | This is the first subpage in the docs. We have access to all of markdown and 8 | html here. 9 | 10 | | We have | Tables | 11 | | ------- | ------ | 12 | | 1, 1 | 1, 2 | 13 | | 2, 1 | 2, 2 | 14 | 15 | We even support Github extensions to the spec like math blocks. 16 | 17 | $$ x^2 + y^2 = z^2 $$ 18 | -------------------------------------------------------------------------------- /www/pyro.yml: -------------------------------------------------------------------------------- 1 | title: Pyro 2 | github: https://github.com/lino-levan/pyro 3 | base: https://pyro.deno.dev 4 | copyright: |- 5 | Copyright © 2023 Lino Le Van 6 | MIT Licensed 7 | header: 8 | left: 9 | - Docs /getting-started/installation 10 | footer: 11 | Learn: 12 | - Introduction / 13 | - Installation /getting-started/installation 14 | Community: 15 | - Discord https://discord.gg/XJMMSSC4Fj 16 | - Support https://github.com/lino-levan/pyro/issues/new 17 | -------------------------------------------------------------------------------- /plugins/demo.tsx: -------------------------------------------------------------------------------- 1 | import { Plugin } from "../src/lib/types.ts"; 2 | 3 | const plugin: Plugin = () => { 4 | return { 5 | header: { 6 | left: Demo, 7 | right: Home, 8 | }, 9 | routes: ["/demo.png"], 10 | handle: async () => { 11 | const req = await fetch( 12 | "https://github.com/lino-levan/pyro/raw/main/www/static/icon.png", 13 | ); 14 | return req; 15 | }, 16 | }; 17 | }; 18 | 19 | export default plugin; 20 | -------------------------------------------------------------------------------- /www/pages/getting-started/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | description: Pyro was designed from the ground up to be no-config and incredibly fast. 4 | index: 1 5 | --- 6 | 7 | In this chapter, you will learn how to get started using Pyro for your own 8 | projects. 9 | 10 | First we will go over [installation](/getting-started/installation) and basic 11 | usage of Pyro. 12 | 13 | Next we will go in depth about 14 | [configuring your Pyro website](/getting-started/configuration). There's not a 15 | lot to configure because the defaults are fantastic! 16 | -------------------------------------------------------------------------------- /deno.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "install": "deno run -Ar install.ts .", 4 | "dev": "cd www && pyro dev", 5 | 6 | // test building websites 7 | "test:deno_json": "cd tests/end_to_end/deno_json && pyro build && cd ../../..", 8 | "test:json_config": "cd tests/end_to_end/json_config && pyro build && cd ../../..", 9 | "test:unocss": "cd tests/end_to_end/unocss && pyro build && cd ../../..", 10 | "test:production": "cd www && pyro build && cd ..", 11 | "test": "deno task test:production && deno task test:json_config && deno task test:deno_json && deno task test:unocss" 12 | }, 13 | "compilerOptions": { 14 | "jsx": "react-jsx", 15 | "jsxImportSource": "https://esm.sh/preact@10.19.3" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pyro(raptor) 2 | 3 | > The static documentation site generator for Deno 4 | 5 | ⚡️ Pyro will help you ship a **beautiful documentation site in no time**. 6 | 7 | 💸 Building a custom tech stack is expensive. Instead, **focus on your content** 8 | and just write Markdown files. 9 | 10 | 💥 Ready for more? **Advanced features** like versioning, i18n, search and theme 11 | customizations are built-in with zero config required. 12 | 13 | 🧐 Pyro at its core is a **static-site generator**. That means it can be 14 | deployed **anywhere**. 15 | 16 | ## Getting started 17 | 18 | See the docs at [pyro.deno.dev](https://pyro.deno.dev) 19 | 20 | ## Maintainers 21 | 22 | - Lino Le Van ([@lino-levan](https://github.com/lino-levan)) 23 | - Dean Srebnik ([@load1n9](https://github.com/load1n9)) 24 | 25 | ## License 26 | 27 | MIT 28 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | name: tests (${{ matrix.os }}) 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, windows-latest, macOS-latest] 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: download deno 19 | uses: denoland/setup-deno@v1 20 | with: 21 | deno-version: v1.x 22 | 23 | - name: check format 24 | if: matrix.os == 'ubuntu-latest' 25 | run: deno fmt --check 26 | 27 | - name: check linting 28 | if: matrix.os == 'ubuntu-latest' 29 | run: deno lint 30 | 31 | - name: install pyro 32 | run: deno task install 33 | 34 | - name: Run tests 35 | run: deno task test 36 | -------------------------------------------------------------------------------- /cli.ts: -------------------------------------------------------------------------------- 1 | import { Command, resolve } from "./deps.ts"; 2 | import { generate } from "./src/generate.ts"; 3 | import { dev } from "./src/dev.ts"; 4 | import { build } from "./src/build.ts"; 5 | 6 | await new Command() 7 | .name("pyro") 8 | .version("0.6.4") 9 | .description("The documentation site generator for Deno") 10 | .command("gen", "Generate a Pyro site.") 11 | .arguments("") 12 | .action((_, p) => { 13 | generate(resolve(p)); 14 | }) 15 | .command("dev", "Start the Pyro dev server") 16 | .option("-p, --port ", "Specify a port for the the dev server") 17 | .option( 18 | "-n, --hostname ", 19 | "Specify a hostname for the dev server", 20 | ) 21 | .action(({ port, hostname }) => { 22 | dev(hostname, port); 23 | }) 24 | .command("build", "Build the static Pyro site") 25 | .action(() => { 26 | build(); 27 | }) 28 | .parse(Deno.args); 29 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-latest 12 | 13 | permissions: 14 | id-token: write # This is required to allow the GitHub Action to authenticate with Deno Deploy. 15 | contents: read 16 | 17 | steps: 18 | - name: Clone repository 19 | uses: actions/checkout@v3 20 | 21 | - name: Download Deno 22 | uses: denoland/setup-deno@v1 23 | with: 24 | deno-version: v1.x 25 | 26 | - name: Install Pyro 27 | run: deno task install 28 | 29 | - name: Build the website 30 | working-directory: ./www 31 | run: pyro build 32 | 33 | - name: Deploy to Deno Deploy 34 | uses: denoland/deployctl@v1 35 | with: 36 | project: pyro 37 | entrypoint: https://deno.land/std/http/file_server.ts 38 | root: ./www/build -------------------------------------------------------------------------------- /install.ts: -------------------------------------------------------------------------------- 1 | // I'm preemptively sorry about this install script. 2 | // Deno has a bug where you can't --config from a remote 3 | // location which makes it impossible afaik to do this 4 | // another way. If you have a better idea, open a PR! 5 | // https://github.com/denoland/deno/issues/13488 6 | 7 | let config_file = "./384y89xnd.jsonc"; 8 | const install_from = Deno.args[0] ?? "https://deno.land/x/pyro"; 9 | 10 | if (install_from === ".") { 11 | config_file = "./deno.jsonc"; 12 | } else { 13 | Deno.writeFileSync( 14 | config_file, 15 | new Uint8Array( 16 | await (await fetch(install_from + "/deno.jsonc")).arrayBuffer(), 17 | ), 18 | ); 19 | } 20 | 21 | const command = new Deno.Command(Deno.execPath(), { 22 | args: [ 23 | "install", 24 | "-Afrg", 25 | "--config", 26 | config_file, 27 | "-n", 28 | "pyro", 29 | install_from + "/cli.ts", 30 | ], 31 | }); 32 | 33 | command.outputSync(); 34 | 35 | if (install_from !== ".") { 36 | Deno.removeSync(config_file); 37 | } 38 | -------------------------------------------------------------------------------- /www/pages/core-concepts/plugins.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Plugins 3 | description: Pyro was designed from the ground up to be no-config and incredibly fast. 4 | index: 4 5 | --- 6 | 7 | While Pyro is designed to have all of the basic features you will need built-in, 8 | there are some cases where one would want to extend the feature set. This can be 9 | achieved with plugins. 10 | 11 | A simple plugin will look like so: 12 | 13 | ```tsx 14 | import { Plugin } from "https://deno.land/x/pyro/src/lib/types.ts"; 15 | 16 | const plugin: Plugin = () => { 17 | return { 18 | header: { 19 | left: Demo, 20 | right: Home, 21 | }, 22 | routes: ["/demo.png"], 23 | handle: async () => { 24 | const req = await fetch( 25 | "https://github.com/lino-levan/pyro/raw/main/www/static/icon.png", 26 | ); 27 | return req; 28 | }, 29 | }; 30 | }; 31 | 32 | export default plugin; 33 | ``` 34 | 35 | More examples can be found in 36 | [the official plugins](https://github.com/lino-levan/pyro/tree/main/plugins). 37 | -------------------------------------------------------------------------------- /www/pages/core-concepts/pages.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Pages 3 | description: Pyro was designed from the ground up to be no-config and incredibly fast. 4 | index: 0 5 | --- 6 | 7 | In this section, we will learn about creating pages in Pyro. 8 | 9 | ## Add a Markdown page​ 10 | 11 | Create a file called `/pages/hello.md`: 12 | 13 | ```md 14 | --- 15 | title: hello page title 16 | description: hello page description 17 | --- 18 | 19 | # Hello 20 | 21 | How are you doing today? 22 | ``` 23 | 24 | Once you save the file, the development server will automatically reload the 25 | changes. Now open [http://localhost:8000/hello](http://localhost:8000/hello) and 26 | you will see the new page you just created! 27 | 28 | ## Routing 29 | 30 | If you are familiar with other static site generators or tools like Next.js, 31 | this "file-sytem routing" approach will feel very similar (because it's the 32 | same)! Let's go through some samples to clarify the behavior: 33 | 34 | - `/pages/index.md` → `[baseUrl]` 35 | - `/pages/foo.md` → `[baseUrl]/foo` 36 | - `/pages/foo/test.md` → `[baseUrl]/foo/test` 37 | - `/pages/foo/index.md` → `[baseUrl]/foo` 38 | -------------------------------------------------------------------------------- /www/pages/core-concepts/sidebar.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Sidebar 3 | description: Pyro was designed from the ground up to be no-config and incredibly fast. 4 | index: 1 5 | --- 6 | 7 | Pyro can **create a sidebar automatically** from your **filesystem structure**: 8 | each folder creates a sidebar category, and each file creates a documentation 9 | link. 10 | 11 | While categories can just be empty, often times we want to fill them with 12 | content like any other documentation link. This can be done by placing an 13 | `index.md` under the folder or creating a file at the same level with the same 14 | name. 15 | 16 | ## Metadata 17 | 18 | Most of the time, we want to place the items on the sidebar using a specific 19 | order (instead of being alphabetical). We can do this using the `index` property 20 | of the frontmatter. Pages are sorted from least index to most index. Pages 21 | without an index property will be treated as having an index of 0. 22 | 23 | This page has an index of 1 (as it is the second page of the category). In 24 | frontmatter, it would look something like this: 25 | 26 | ```md 27 | --- 28 | index: 1 29 | --- 30 | ``` 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Lino Le Van 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import type { JSX } from "../../deps.ts"; 2 | export type { JSX } from "../../deps.ts"; 3 | 4 | type MaybePromise = T | Promise; 5 | 6 | export interface Config { 7 | title: string; 8 | base?: string; 9 | github?: string; 10 | copyright?: string; 11 | header?: { left?: string[]; right?: string[] }; 12 | footer?: Record; 13 | hide_navbar?: boolean; 14 | plugins?: string[]; 15 | } 16 | 17 | export type PluginResult = { 18 | /** 19 | * Header bar elements 20 | */ 21 | header?: { 22 | left?: JSX.Element; 23 | right?: JSX.Element; 24 | }; 25 | /** 26 | * A method that returns a list of routes to handle. 27 | * This has to be a finite list for static site building. 28 | */ 29 | routes?: string[]; 30 | /** 31 | * The method for actually handling whatever route 32 | */ 33 | handle?: (req: Request) => MaybePromise; 34 | }; 35 | 36 | export type Plugin = () => MaybePromise; 37 | 38 | export interface Magic { 39 | background: string; 40 | } 41 | 42 | export interface RouteMap { 43 | title: string; 44 | url: string; 45 | index: number; 46 | sub_route_map?: RouteMap[]; 47 | } 48 | 49 | export type FileTypes = "md" | "tsx"; 50 | -------------------------------------------------------------------------------- /www/pages/core-concepts/unocss.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: UnoCSS 3 | description: Pyro uses UnoCSS to provide an atomic CSS engine. 4 | index: 3 5 | --- 6 | 7 | Pyro uses UnoCSS for styling, both internally and for user code. By default, the 8 | configuration that Pyro uses for UnoCSS looks something like: 9 | 10 | ```ts 11 | import { presetUno } from "https://esm.sh/@unocss/preset-uno@0.58.0"; 12 | 13 | export default { 14 | presets: [ 15 | presetUno({ 16 | dark: "media", 17 | }), 18 | ], 19 | }; 20 | ``` 21 | 22 | Some users would prefer to have more UnoCSS features. This is supported with 23 | zero extra configuration outside of just making the file! At the top level of 24 | your site, just add a `uno.config.ts` file. Below is an example of configuring 25 | UnoCSS to add the `presetTypography` plugin. Make sure not to import from the 26 | main `uno` package, as that is unsupported. 27 | 28 | ```ts 29 | // uno.config.ts 30 | import { presetUno } from "https://esm.sh/@unocss/preset-uno@0.58.0"; 31 | import { presetTypography } from "https://esm.sh/@unocss/preset-typography@0.58.0"; 32 | 33 | export default { 34 | presets: [ 35 | presetUno({ 36 | dark: "media", 37 | }), 38 | presetTypography(), 39 | ], 40 | }; 41 | ``` 42 | -------------------------------------------------------------------------------- /www/pages/guides/deployment/deno-deploy.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Deno Deploy 3 | description: Pyro was designed from the ground up to be no-config and incredibly fast. 4 | index: 0 5 | --- 6 | 7 | Pyro provides an easy way to publish to Deno Deploy using Github Actions, which 8 | comes for free with every GitHub repository. 9 | 10 | Place the following file in `.github/workflows/deploy.yml` and replace 11 | `DENO_DEPLOY_PROJECT` with the name of your project on Deno Deploy. 12 | 13 | ```yaml 14 | name: Deploy 15 | 16 | on: 17 | push: 18 | branches: [main] 19 | pull_request: 20 | branches: [main] 21 | 22 | jobs: 23 | deploy: 24 | runs-on: ubuntu-latest 25 | 26 | permissions: 27 | id-token: write # This is required to allow the GitHub Action to authenticate with Deno Deploy. 28 | contents: read 29 | 30 | steps: 31 | - name: Clone repository 32 | uses: actions/checkout@v3 33 | 34 | - name: Download Deno 35 | uses: denoland/setup-deno@v1 36 | with: 37 | deno-version: v1.x 38 | 39 | - name: Install Pyro 40 | run: deno run -Ar https://deno.land/x/pyro/install.ts 41 | 42 | - name: Build the website 43 | run: pyro build 44 | 45 | - name: Deploy to Deno Deploy 46 | uses: denoland/deployctl@v1 47 | with: 48 | project: DENO_DEPLOY_PROJECT 49 | entrypoint: https://deno.land/std/http/file_server.ts 50 | root: ./build 51 | ``` 52 | -------------------------------------------------------------------------------- /www/pages/guides/deployment/github-pages.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Github Pages 3 | description: Pyro was designed from the ground up to be no-config and incredibly fast. 4 | index: 1 5 | --- 6 | 7 | Pyro provides an easy way to publish to Github Pages using Github Actions, which 8 | comes for free with every GitHub repository. 9 | 10 | Place the following file in `.github/workflows/deploy.yml`. 11 | 12 | ```yaml 13 | name: Deploy 14 | 15 | on: 16 | push: 17 | branches: [main] 18 | pull_request: 19 | branches: [main] 20 | 21 | jobs: 22 | deploy: 23 | runs-on: ubuntu-latest 24 | 25 | permissions: 26 | id-token: write 27 | pages: write 28 | 29 | environment: 30 | name: github-pages 31 | url: ${{ steps.deployment.outputs.page_url }} 32 | 33 | steps: 34 | - name: Checkout 35 | uses: actions/checkout@v3 36 | 37 | - name: Download Deno 38 | uses: denoland/setup-deno@v1 39 | with: 40 | deno-version: v1.x 41 | 42 | - name: Install Pyro 43 | run: deno run -Ar https://deno.land/x/pyro/install.ts 44 | 45 | - name: Build the website 46 | run: pyro build 47 | 48 | - name: Setup Pages 49 | uses: actions/configure-pages@v3 50 | 51 | - name: Upload artifact 52 | uses: actions/upload-pages-artifact@v1 53 | with: 54 | path: './build' 55 | 56 | - name: Deploy to GitHub Pages 57 | id: deployment 58 | uses: actions/deploy-pages@v1 59 | ``` 60 | -------------------------------------------------------------------------------- /src/lib/magic.ts: -------------------------------------------------------------------------------- 1 | // This file is for all of the "magic" autoconfig we do 2 | // For example, we select the background color from the logo 3 | // No config needed! 4 | import { decode } from "../../deps.ts"; 5 | import type { Magic } from "./types.ts"; 6 | 7 | function saturation(r: number, g: number, b: number) { 8 | r = r / 255; 9 | g = g / 255; 10 | b = b / 255; 11 | 12 | const max = Math.max(r, g, b); 13 | const min = Math.min(r, g, b); 14 | 15 | const lum = max - min; 16 | 17 | if (lum < 0.5) { 18 | return (max - min) / (max + min); 19 | } else { 20 | return (max - min) / (2 - max - min); 21 | } 22 | } 23 | 24 | function componentToHex(c: number) { 25 | const hex = c.toString(16); 26 | return hex.length == 1 ? "0" + hex : hex; 27 | } 28 | 29 | function getBackground() { 30 | try { 31 | const logo = Deno.readFileSync("./static/icon.png"); 32 | const bytes = decode(logo); 33 | 34 | let brightest_color = "#000000"; 35 | let max_brightness = 0; 36 | 37 | for (let i = 0; i < bytes.image.length; i += 4) { 38 | const [r, g, b, a] = [ 39 | bytes.image[i], 40 | bytes.image[i + 1], 41 | bytes.image[i + 2], 42 | bytes.image[i + 3], 43 | ]; 44 | const brightness = saturation(r, g, b) * a; 45 | 46 | if (brightness > max_brightness) { 47 | max_brightness = brightness; 48 | brightest_color = `#${componentToHex(r)}${componentToHex(g)}${ 49 | componentToHex(b) 50 | }`; 51 | } 52 | } 53 | 54 | return brightest_color; 55 | } catch { 56 | return "white"; 57 | } 58 | } 59 | 60 | export function getMagic(): Magic { 61 | return { 62 | background: getBackground(), 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /src/generate.ts: -------------------------------------------------------------------------------- 1 | import { join } from "../deps.ts"; 2 | 3 | /** 4 | * Generate the skeleton for a pyro documentation site 5 | */ 6 | export async function generate(path: string) { 7 | Deno.mkdirSync(join(path, "pages", "getting-started"), { recursive: true }); 8 | Deno.mkdirSync(join(path, "static"), { recursive: true }); 9 | 10 | Deno.writeTextFileSync( 11 | join(path, "pyro.yml"), 12 | "title: Pyro Site\ngithub: https://github.com/lino-levan/pyro", 13 | ); 14 | 15 | Deno.writeTextFileSync( 16 | join(path, "pages", "index.md"), 17 | `--- 18 | title: Hello World 19 | description: My first Pyro page 20 | index: 0 21 | --- 22 | 23 | How are you doing today? 24 | `, 25 | ); 26 | 27 | Deno.writeTextFileSync( 28 | join(path, "pages", "getting-started", "index.md"), 29 | `--- 30 | title: Getting Started 31 | description: My first Pyro directory 32 | index: 1 33 | --- 34 | `, 35 | ); 36 | 37 | Deno.writeTextFileSync( 38 | join(path, "pages", "getting-started", "submenu.md"), 39 | `--- 40 | title: Submenu 41 | description: My first Pyro directory 42 | index: 0 43 | --- 44 | 45 | This is the first subpage in the docs. We have access to all of markdown and html here. 46 | 47 | | We have | Tables | 48 | | ----------- | ----------- | 49 | | 1, 1 | 1, 2 | 50 | | 2, 1 | 2, 2 | 51 | 52 | We even support Github extensions to the spec like math blocks. 53 | 54 | $$ 55 | x^2 + y^2 = z^2 56 | $$ 57 | `, 58 | ); 59 | 60 | const icon = await fetch( 61 | "https://raw.githubusercontent.com/lino-levan/pyro/main/www/static/icon.png", 62 | ); 63 | 64 | Deno.writeFileSync( 65 | join(path, "static", "icon.png"), 66 | new Uint8Array(await icon.arrayBuffer()), 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /src/lib/route_map.ts: -------------------------------------------------------------------------------- 1 | import { extract, join, posix, resolve, win32 } from "../../deps.ts"; 2 | import { readFileSync } from "../utils.tsx"; 3 | import type { RouteMap } from "./types.ts"; 4 | 5 | export function resolve_file(path: string) { 6 | return readFileSync( 7 | resolve(path + ".md"), 8 | resolve(path + ".tsx"), 9 | resolve(path, "index.md"), 10 | resolve(path, "index.tsx"), 11 | ); 12 | } 13 | 14 | export let global_route_map: RouteMap[] = []; 15 | 16 | export function get_route_map(directory: string, top_level = false) { 17 | const route_map = []; 18 | 19 | for (const entry of Deno.readDirSync(directory)) { 20 | if (entry.name.startsWith("_")) continue; 21 | 22 | const [file_type, markdown] = resolve_file( 23 | join(directory, entry.name.split(".")[0]), 24 | ); 25 | if (file_type === "tsx") continue; 26 | 27 | const frontmatter = extract(markdown); 28 | const extracted = (Deno.build.os == "windows" 29 | ? posix.fromFileUrl(win32.toFileUrl(posix.resolve(directory, entry.name))) 30 | : resolve(directory, entry.name)).match( 31 | /(?:.+?\/pages(.+)\.)|(?:.+?\/pages(.+))/, 32 | )!; 33 | const url = extracted[1] || extracted[2]; 34 | 35 | if (url.includes("index") && !top_level) { 36 | continue; 37 | } 38 | 39 | const route: RouteMap = { 40 | title: frontmatter.attrs.title as string, 41 | url: url.replace("index", ""), 42 | index: frontmatter.attrs.index as number ?? 0, 43 | }; 44 | 45 | // console.log(route) 46 | 47 | if (entry.isDirectory) { 48 | route.sub_route_map = get_route_map(join(directory, entry.name)); 49 | } 50 | 51 | route_map.push(route); 52 | } 53 | 54 | route_map.sort((a, b) => a.index - b.index); 55 | 56 | if (top_level) { 57 | global_route_map = route_map; 58 | } 59 | 60 | return route_map; 61 | } 62 | -------------------------------------------------------------------------------- /src/lib/rehype_starry_night.ts: -------------------------------------------------------------------------------- 1 | import { 2 | all, 3 | createStarryNight, 4 | Element, 5 | ElementContent, 6 | Node, 7 | type Plugin, 8 | Root, 9 | toString, 10 | visit, 11 | } from "../../deps.ts"; 12 | 13 | const starryNight = await createStarryNight(all); 14 | 15 | export function rehypeStarryNight(): Plugin { 16 | const prefix = "language-"; 17 | 18 | return (tree) => { 19 | visit(tree, "element", (node: Element, index: number, parent: Element) => { 20 | if (!parent || index === null || node.tagName !== "pre") { 21 | return; 22 | } 23 | 24 | const head = node.children[0]; 25 | 26 | if ( 27 | !head || 28 | head.type !== "element" || 29 | head.tagName !== "code" || 30 | !head.properties 31 | ) { 32 | return; 33 | } 34 | 35 | const classes = head.properties.className; 36 | 37 | if (!Array.isArray(classes)) return; 38 | 39 | const language = classes.find( 40 | (d) => typeof d === "string" && d.startsWith(prefix), 41 | ); 42 | 43 | if (typeof language !== "string") return; 44 | 45 | const scope = starryNight.flagToScope(language.slice(prefix.length)); 46 | 47 | // Maybe warn? 48 | if (!scope) return; 49 | 50 | // @ts-ignore idk I didn't write this and typescript is complaining 51 | const fragment = starryNight.highlight(toString(head), scope); 52 | const children = fragment.children as Array; 53 | 54 | parent.children.splice(index, 1, { 55 | type: "element", 56 | tagName: "div", 57 | properties: { 58 | className: [ 59 | "highlight", 60 | "highlight-" + scope.replace(/^source\./, "").replace(/\./g, "-"), 61 | ], 62 | }, 63 | children: [{ 64 | type: "element", 65 | tagName: "pre", 66 | properties: {}, 67 | children, 68 | }], 69 | }); 70 | }); 71 | }; 72 | } 73 | -------------------------------------------------------------------------------- /www/pages/getting-started/configuration.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Configuration 3 | description: Pyro was designed from the ground up to be no-config and incredibly fast. 4 | index: 1 5 | --- 6 | 7 | Pyro has a very simplistic view of configurations, there is only one file you 8 | need to worry about (being `pyro.yml`/`pyro.toml`/`pyro.json`/`pyro.jsonc`). 9 | 10 | ## What goes into a Pyro configuration file? 11 | 12 | This is just a standard configuration file. Inside you will define all 13 | properties related to pyro and plugins you may install. 14 | 15 | This is an example of a `pyro.yml` configuration file: 16 | 17 | ```yaml 18 | # The title of the site 19 | title: Pyro 20 | 21 | # The base URL to use for open graph absolute URLs (optional) 22 | base: https://pyro.deno.dev 23 | 24 | # The Github repository for the documentation site (optional) 25 | github: https://github.com/lino-levan/pyro 26 | 27 | # Any copyright information you want to include in the footer (optional) 28 | copyright: |- 29 | Copyright © 2023 Lino Le Van 30 | MIT Licensed 31 | 32 | # Links in the header (optional) 33 | header: 34 | # links on the left side 35 | left: 36 | - Docs /getting-started/installation 37 | 38 | # links on the right side 39 | right: 40 | - Bonus Content https://stackoverflow.com 41 | 42 | # Links in the footer (optional) 43 | footer: 44 | # Header of the column 45 | Learn: 46 | - Introduction / 47 | - Installation /getting-started/installation 48 | Community: 49 | - Discord https://discord.gg/XJMMSSC4Fj 50 | - Support https://github.com/lino-levan/pyro/issues/new 51 | 52 | # Hide the navigation bar on the left from rendering (optional) 53 | hide_navbar: true 54 | 55 | # Any plugins you want to be used (optional) 56 | plugin: 57 | - https://deno.land/x/pyro/plugins/demo.tsx 58 | ``` 59 | 60 | ## How do I configure individual pages? 61 | 62 | You can configure individual page metadata using markdown frontmatter. We 63 | support yaml/toml/json/jsonc. 64 | 65 | Here is an example in YAML: 66 | 67 | ```md 68 | --- 69 | title: Title of Page 70 | description: Metadata description of page 71 | hide_navbar: true 72 | index: 0 73 | --- 74 | ``` 75 | 76 | - `title` - The title of the page which also shows up as a large header 77 | - `description` - The description of the page for SEO and embeds 78 | - `hide_navbar` - Hide the navbar from this page 79 | - `index` - Used in determining [sidebar order](/guides/docs/sidebar) 80 | -------------------------------------------------------------------------------- /src/lib/sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { ChevronDown } from "../../deps.ts"; 2 | import type { RouteMap } from "./types.ts"; 3 | import { global_route_map } from "./route_map.ts"; 4 | import { readConfig } from "../utils.tsx"; 5 | 6 | function simplify(url: string) { 7 | return url.replaceAll("/", "").replaceAll("-", ""); 8 | } 9 | 10 | export function Sidebar( 11 | props: { route_map?: RouteMap[]; class: string; route: string }, 12 | ) { 13 | const config = readConfig(); 14 | const route_map = props.route_map ?? global_route_map; 15 | return ( 16 |
17 | {(!props.route_map && props.route === "_PYRO_SHOW_LOGO") && ( 18 | 19 |

20 | 21 | {config.title} 22 |

23 |
24 | )} 25 | {route_map.map((route) => ( 26 | <> 27 | {route.sub_route_map 28 | ? ( 29 |
33 | 34 | 38 | {route.title} 39 | 40 | 45 | 46 | 51 |
52 | ) 53 | : ( 54 | 58 | {route.title} 59 | {route.sub_route_map && } 60 | 61 | )} 62 | 63 | ))} 64 |
65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /deps.ts: -------------------------------------------------------------------------------- 1 | export type { JSX } from "https://esm.sh/preact@10.19.3"; 2 | export { renderToString } from "https://esm.sh/preact-render-to-string@6.3.1"; 3 | 4 | export { 5 | fromFileUrl, 6 | join, 7 | posix, 8 | resolve, 9 | toFileUrl, 10 | win32, 11 | } from "https://deno.land/std@0.210.0/path/mod.ts"; 12 | export { walkSync } from "https://deno.land/std@0.210.0/fs/walk.ts"; 13 | export { parse as parseYaml } from "https://deno.land/std@0.210.0/yaml/mod.ts"; 14 | export { parse as parseJsonc } from "https://deno.land/std@0.210.0/jsonc/mod.ts"; 15 | export { parse as parseToml } from "https://deno.land/std@0.210.0/toml/mod.ts"; 16 | export { copySync } from "https://deno.land/std@0.210.0/fs/copy.ts"; 17 | export { serveDir } from "https://deno.land/std@0.210.0/http/file_server.ts"; 18 | export { extract } from "https://deno.land/std@0.210.0/front_matter/any.ts"; 19 | export { existsSync } from "https://deno.land/std@0.210.0/fs/exists.ts"; 20 | 21 | export { launch } from "https://deno.land/x/astral@0.3.2/mod.ts"; 22 | 23 | export * as esbuild from "https://deno.land/x/esbuild@v0.19.2/mod.js"; 24 | export { denoPlugins } from "https://deno.land/x/esbuild_deno_loader@0.8.2/mod.ts"; 25 | 26 | export { createGenerator } from "https://esm.sh/@unocss/core@0.58.0"; 27 | export { presetUno } from "https://esm.sh/@unocss/preset-uno@0.58.0"; 28 | 29 | export { decode } from "https://deno.land/x/pngs@0.1.1/mod.ts"; 30 | 31 | export { default as Github } from "https://deno.land/x/tabler_icons_tsx@0.0.6/tsx/brand-github.tsx"; 32 | export { default as ExternalLink } from "https://deno.land/x/tabler_icons_tsx@0.0.6/tsx/external-link.tsx"; 33 | export { default as ChevronDown } from "https://deno.land/x/tabler_icons_tsx@0.0.6/tsx/chevron-down.tsx"; 34 | export { default as IconMenu2 } from "https://deno.land/x/tabler_icons_tsx@0.0.6/tsx/menu-2.tsx"; 35 | 36 | export { Command } from "https://deno.land/x/cliffy@v1.0.0-rc.3/command/mod.ts"; 37 | 38 | export { type Plugin, unified } from "https://esm.sh/unified@11.0.4"; 39 | export { default as remarkParse } from "https://esm.sh/remark-parse@11.0.0"; 40 | export { default as remarkGfm } from "https://esm.sh/remark-gfm@4.0.0"; 41 | export { default as remarkRehype } from "https://esm.sh/remark-rehype@11.0.0"; 42 | export { default as rehypeStringify } from "https://esm.sh/rehype-stringify@10.0.0"; 43 | 44 | export { 45 | all, 46 | createStarryNight, 47 | type Grammar, 48 | } from "https://esm.sh/@wooorm/starry-night@3.2.0"; 49 | export { toString } from "https://esm.sh/hast-util-to-string@3.0.0"; 50 | export { visit } from "https://esm.sh/unist-util-visit@5.0.0"; 51 | export { 52 | type Element, 53 | type ElementContent, 54 | type Node, 55 | type Root, 56 | } from "https://esm.sh/v131/@types/hast@3.0.0/index.d.ts"; 57 | -------------------------------------------------------------------------------- /www/pages/core-concepts/markdown-features.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown Features 3 | description: Pyro was designed from the ground up to be no-config and incredibly fast. 4 | index: 2 5 | --- 6 | 7 | Pyro supports the entirety of the markdown spec as well as many common 8 | extensions. 9 | 10 | Below are some examples from the excellent 11 | [markdownguide.org](https://www.markdownguide.org/cheat-sheet/) 12 | 13 | ## Headings 14 | 15 | ```md 16 | ### H3 17 | 18 | #### H4 19 | 20 | ##### H5 21 | ``` 22 | 23 | ### H3 24 | 25 | #### H4 26 | 27 | ##### H5 28 | 29 | ## Formatted text 30 | 31 | ```md 32 | **bold text** 33 | 34 | _italicized text_ 35 | 36 | > blockquote 37 | 38 | [text with link](https://www.example.com) 39 | 40 | ~~Crossed out text~~ 41 | ``` 42 | 43 | **bold text** 44 | 45 | _italicized text_ 46 | 47 | > blockquote 48 | 49 | [text with link](https://www.example.com) 50 | 51 | ~~Crossed out text~~ 52 | 53 | ## Lists 54 | 55 | ```md 56 | ### Numbered list 57 | 58 | 1. First item 59 | 2. Second item 60 | 3. Third item 61 | 62 | ### Unordered list 63 | 64 | - First item 65 | - Second item 66 | - Third item 67 | 68 | ### Checklist 69 | 70 | - [x] Write the press release 71 | - [ ] Update the website 72 | - [ ] Contact the media 73 | ``` 74 | 75 | ### Numbered list 76 | 77 | 1. First item 78 | 2. Second item 79 | 3. Third item 80 | 81 | ### Unordered list 82 | 83 | - First item 84 | - Second item 85 | - Third item 86 | 87 | ### Checklist 88 | 89 | - [x] Write the press release 90 | - [ ] Update the website 91 | - [ ] Contact the media 92 | 93 | ## Code blocks 94 | 95 | ````md 96 | `code` 97 | 98 | ```json 99 | { 100 | "firstName": "John", 101 | "lastName": "Smith", 102 | "age": 25 103 | } 104 | `` ` 105 | ``` 106 | ```` 107 | 108 | `code` 109 | 110 | ```json 111 | { 112 | "firstName": "John", 113 | "lastName": "Smith", 114 | "age": 25 115 | } 116 | ``` 117 | 118 | ## HTML 119 | 120 | ```md 121 | 122 | 123 | 125 | ``` 126 | 127 | 128 | 129 | 131 | 132 | ## Separator 133 | 134 | ```md 135 | --- 136 | ``` 137 | 138 | --- 139 | 140 | ## Tables 141 | 142 | ```md 143 | | Syntax | Description | 144 | | --------- | ----------- | 145 | | Header | Title | 146 | | Paragraph | Text | 147 | ``` 148 | 149 | | Syntax | Description | 150 | | --------- | ----------- | 151 | | Header | Title | 152 | | Paragraph | Text | 153 | -------------------------------------------------------------------------------- /www/pages/getting-started/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | description: Pyro was designed from the ground up to be no-config and incredibly fast. 4 | index: 0 5 | --- 6 | 7 | Pyro(raptor) is essentially just a [Deno CLI tool](https://deno.land). 8 | 9 | ## Requirements 10 | 11 | - Deno version 1.31.2 or above (which can be checked by running 12 | `deno --version`). You can use `deno upgrade` or [tea](https://tea.xyz) for 13 | managing multiple versions of Deno on a single machine (if neccessary). 14 | 15 | If these requirements are met, you should be able to install the CLI: 16 | 17 | ```bash 18 | deno run -Ar https://deno.land/x/pyro/install.ts 19 | ``` 20 | 21 | ## Scaffold your first site 22 | 23 | With the CLI installed, scaffolding your first Pyro site is easy: 24 | 25 | ``` 26 | pyro gen my-pyro-site 27 | ``` 28 | 29 | In the future, this will support community-made templates but for now there are 30 | no other configuration options available. 31 | 32 | ## Project structure 33 | 34 | Assuming you are using the default template and you named your site 35 | `my-pyro-site`, you will see the following files generated under the 36 | `my-pyro-site/` directory. 37 | 38 | ``` 39 | my-pyro-site 40 | ├── pages 41 | │ ├── getting-started 42 | │ │ ├── index.md 43 | │ │ └── submenu.md 44 | │ └── index.md 45 | ├── static 46 | │ └── icon.png 47 | └── pyro.yml 48 | ``` 49 | 50 | ### Project structure rundown 51 | 52 | - `/pages/` - Contains the markdown files for your documentation. We will go 53 | into more detail into how this is organized in 54 | [Configuration](/getting-started/configuration). 55 | - `/static/` - Contains any static assets that you may need (images, audio, 56 | etc.) 57 | - `pyro.yml` - The only real configuration file for your site. Your site name as 58 | well as any plugins will be defined here. Again there is more detail in 59 | [Configuration](/getting-started/configuration). 60 | 61 | ## Running the development server 62 | 63 | To preview your changes as you edit the files, you can run a local development 64 | server that will serve your website and reflect the latest changes. 65 | 66 | ```bash 67 | cd my-pyro-site 68 | pyro dev 69 | ``` 70 | 71 | By default, a browser window will open at http://localhost:8000. 72 | 73 | Congratulations! You have just created your first Pyro site! Browse around the 74 | site to see what's available. 75 | 76 | ## Build 77 | 78 | Pyro is a modern static website generator so we need to build the website into a 79 | directory of static contents and put it on a web server so that it can be 80 | viewed. To build the website: 81 | 82 | ```bash 83 | pyro build 84 | ``` 85 | 86 | and contents will be generated within the `/build` directory, which can be 87 | copied to any static file hosting service like 88 | [GitHub pages](https://pages.github.com/), [Vercel](https://vercel.com/) or 89 | [Netlify](https://www.netlify.com/). Check out the docs on 90 | [deployment](/guides/deployment) for more details. 91 | 92 | ## Problems?​ 93 | 94 | Ask for help on our 95 | [Github repository](https://github.com/lino-levan/pyro/issues/new) or our 96 | [Discord server](https://discord.gg/XJMMSSC4Fj). 97 | -------------------------------------------------------------------------------- /src/dev.ts: -------------------------------------------------------------------------------- 1 | import { resolve, serveDir } from "../deps.ts"; 2 | import { render } from "./lib/render.ts"; 3 | import { getMagic } from "./lib/magic.ts"; 4 | import { CSS } from "./lib/css.ts"; 5 | import { loadPlugins, readConfig } from "./utils.tsx"; 6 | 7 | export async function dev(hostname = "0.0.0.0", port = 8000) { 8 | let BUILD_ID = Math.random().toString(); 9 | 10 | Deno.serve({ 11 | hostname, 12 | port, 13 | }, async (req) => { 14 | const url = new URL(req.url); 15 | const pathname = url.pathname; 16 | 17 | const config = readConfig(); 18 | const plugins = config.plugins ? await loadPlugins(config.plugins) : []; 19 | 20 | for (const plugin of plugins) { 21 | if (!plugin.routes || !plugin.handle) continue; 22 | 23 | if (plugin.routes.includes(pathname)) { 24 | return plugin.handle(req); 25 | } 26 | } 27 | 28 | // Handle the bundled css 29 | if (pathname === "/_pyro/bundle.css") { 30 | return new Response(CSS, { 31 | headers: { 32 | "Content-Type": "text/css", 33 | }, 34 | }); 35 | } 36 | 37 | // Handle the reload js 38 | if (pathname === "/_pyro/reload.js") { 39 | return new Response( 40 | `new EventSource("/_pyro/reload").addEventListener("message", function listener(e) { if (e.data !== "${BUILD_ID}") { this.removeEventListener('message', listener); location.reload(); } });`, 41 | { 42 | headers: { 43 | "Content-Type": "text/css", 44 | }, 45 | }, 46 | ); 47 | } 48 | 49 | if (pathname === "/_pyro/reload") { 50 | let timerId: number | undefined = undefined; 51 | const body = new ReadableStream({ 52 | start(controller) { 53 | controller.enqueue(`data: ${BUILD_ID}\nretry: 100\n\n`); 54 | timerId = setInterval(() => { 55 | controller.enqueue(`data: ${BUILD_ID}\n\n`); 56 | }, 1000); 57 | }, 58 | cancel() { 59 | if (timerId !== undefined) { 60 | clearInterval(timerId); 61 | } 62 | }, 63 | }); 64 | return new Response(body.pipeThrough(new TextEncoderStream()), { 65 | headers: { 66 | "content-type": "text/event-stream", 67 | }, 68 | }); 69 | } 70 | 71 | // We're supposed to ignore hidden paths 72 | if (pathname.includes("/_")) { 73 | return new Response("404 File Not Found", { 74 | status: 404, 75 | }); 76 | } 77 | 78 | // We're supposed to serve a static file 79 | if (pathname.includes(".")) { 80 | try { 81 | return serveDir(req, { 82 | fsRoot: "./static", 83 | quiet: true, 84 | }); 85 | } catch (err) { 86 | console.log(err); 87 | return new Response("404 File Not Found", { 88 | status: 404, 89 | }); 90 | } 91 | } 92 | 93 | return new Response( 94 | await render( 95 | getMagic(), 96 | resolve("pages", pathname.slice(1)), 97 | plugins, 98 | true, 99 | ), 100 | { 101 | headers: { 102 | "Content-Type": "text/html; charset=utf-8", 103 | }, 104 | }, 105 | ); 106 | }); 107 | 108 | const watcher = Deno.watchFs(".", { recursive: true }); 109 | 110 | for await (const _ of watcher) { 111 | BUILD_ID = Math.random().toString(); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/lib/render.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createGenerator, 3 | existsSync, 4 | extract, 5 | join, 6 | presetUno, 7 | renderToString, 8 | resolve, 9 | } from "../../deps.ts"; 10 | 11 | import { Footer, Header, page } from "./page.tsx"; 12 | import { get_route_map, resolve_file } from "./route_map.ts"; 13 | import type { Magic, PluginResult } from "./types.ts"; 14 | import { getHeaderElements, importBuild, readConfig } from "../utils.tsx"; 15 | 16 | const unoConfig = existsSync("./uno.config.ts") 17 | ? (await importBuild("./uno.config.ts")).default 18 | : { 19 | presets: [ 20 | presetUno({ 21 | dark: "media", 22 | }), 23 | ], 24 | }; 25 | 26 | const uno = createGenerator(unoConfig); 27 | 28 | async function applyUno(html: string) { 29 | // Apply uno 30 | const warn = console.warn; 31 | console.warn = () => {}; 32 | const { css } = await uno.generate(html); 33 | html = html.replace("", ``); 34 | console.warn = warn; 35 | return html; 36 | } 37 | 38 | export async function render( 39 | magic: Magic, 40 | path: string, 41 | plugins: PluginResult[], 42 | dev = false, 43 | ) { 44 | const config = readConfig(); 45 | const base_path = resolve("pages", path); 46 | const [file_type, markdown] = resolve_file(base_path); 47 | 48 | if (file_type === "tsx") { 49 | const indexPath = join(base_path, "index.tsx"); 50 | const entrypoint = existsSync(indexPath) ? indexPath : base_path + ".tsx"; 51 | 52 | const { default: userPage, config: userConfig } = await importBuild( 53 | entrypoint, 54 | ); 55 | 56 | return await applyUno( 57 | "\n" + renderToString( 58 | await page({ 59 | page: { title: config.title, description: userConfig.description }, 60 | options: { 61 | route: "/" + path, 62 | config, 63 | dev, 64 | magic, 65 | body: renderToString( 66 | await userPage({ 67 | header: Header({ 68 | title: config.title, 69 | header: getHeaderElements(config, plugins), 70 | github: config.github, 71 | }), 72 | footer: config.footer 73 | ? Footer({ 74 | copyright: config.copyright, 75 | github: config.github, 76 | footer: config.footer, 77 | }) 78 | : undefined, 79 | }), 80 | ), 81 | }, 82 | }), 83 | ), 84 | ); 85 | } 86 | 87 | const frontmatter = extract(markdown); 88 | get_route_map(resolve("pages"), true); 89 | 90 | const title = frontmatter.attrs.title as string ?? "No Title"; 91 | const description = frontmatter.attrs.description as string ?? 92 | "No Description"; 93 | const hide_navbar = frontmatter.attrs.hide_navbar 94 | ? !!frontmatter.attrs.hide_navbar 95 | : undefined; 96 | 97 | const html = "\n" + renderToString( 98 | await page({ 99 | page: { title, description, hide_navbar }, 100 | options: { 101 | markdown, 102 | route: "/" + path, 103 | config, 104 | magic, 105 | file_type, 106 | dev, 107 | header: getHeaderElements(config, plugins), 108 | }, 109 | }), 110 | ); 111 | 112 | return await applyUno(html); 113 | } 114 | -------------------------------------------------------------------------------- /src/build.ts: -------------------------------------------------------------------------------- 1 | import { 2 | copySync, 3 | esbuild, 4 | launch, 5 | posix, 6 | resolve, 7 | serveDir, 8 | walkSync, 9 | win32, 10 | } from "../deps.ts"; 11 | import { render } from "./lib/render.ts"; 12 | import { getMagic } from "./lib/magic.ts"; 13 | import { CSS } from "./lib/css.ts"; 14 | import { loadPlugins, readConfig } from "./utils.tsx"; 15 | 16 | async function screenshot() { 17 | console.log("Embedding pages..."); 18 | 19 | const controller = new AbortController(); 20 | const port = 8412; 21 | 22 | const server = Deno.serve({ signal: controller.signal, port }, (req) => { 23 | return serveDir(req, { 24 | fsRoot: "build", 25 | quiet: true, 26 | }); 27 | }); 28 | 29 | // TODO(lino-levan): Just do this better. What is this? 30 | const browser = await launch({ 31 | args: [`--window-size=1920,1080`], 32 | }); 33 | const page = await browser.newPage(); 34 | const celestial = page.unsafelyGetCelestialBindings(); 35 | await celestial.Emulation.setScrollbarsHidden({ hidden: true }); 36 | 37 | for ( 38 | const entry of walkSync("./build", { 39 | includeDirs: false, 40 | match: [/^.+index\.html$/], 41 | }) 42 | ) { 43 | const url = new URL(entry.path.slice(5, -10), `http://localhost:${port}`); 44 | console.log("Embedding:", url.href); 45 | await page.goto(url.href, { waitUntil: "networkidle0" }); 46 | const screenshot = await page.screenshot(); 47 | Deno.writeFileSync( 48 | resolve(entry.path.slice(0, -10), "embed.png"), 49 | screenshot, 50 | ); 51 | } 52 | 53 | await browser.close(); 54 | 55 | controller.abort(); 56 | await server.finished; 57 | } 58 | 59 | export async function build() { 60 | try { 61 | Deno.removeSync("build", { 62 | recursive: true, 63 | }); 64 | } catch { 65 | // no-op 66 | } 67 | 68 | copySync("./static", "./build"); 69 | 70 | Deno.mkdirSync("./build/_pyro"); 71 | Deno.writeTextFileSync("./build/_pyro/bundle.css", CSS); 72 | 73 | const config = readConfig(); 74 | const magic = getMagic(); 75 | const plugins = config.plugins ? await loadPlugins(config.plugins) : []; 76 | 77 | for (const plugin of plugins) { 78 | if (!plugin.routes || !plugin.handle) continue; 79 | 80 | for (const route of plugin.routes) { 81 | console.log(`Created plugin route: ${route}`); 82 | const response = new Uint8Array( 83 | await (await plugin.handle( 84 | new Request("http://localhost:8000" + route), 85 | )).arrayBuffer(), 86 | ); 87 | const path = resolve("build", route.slice(1)); 88 | 89 | if (route.includes(".")) { 90 | await Deno.writeFile(path, response); 91 | } else { 92 | await Deno.mkdir(path, { recursive: true }); 93 | await Deno.writeFile(resolve(path, "index.html"), response); 94 | } 95 | } 96 | } 97 | 98 | for ( 99 | const entry of walkSync("./pages", { includeDirs: false, skip: [/\/_/] }) 100 | ) { 101 | console.log(`Rendered route: ${entry.path}`); 102 | 103 | const extracted = (Deno.build.os == "windows" 104 | ? posix.fromFileUrl(win32.toFileUrl(posix.resolve(entry.path))) 105 | : resolve(entry.path)).match( 106 | /.+?\/pages(.+)\./, 107 | )!; 108 | 109 | const folder = extracted[1].slice(1).replace("index", ""); 110 | Deno.mkdirSync(resolve("build", folder), { recursive: true }); 111 | Deno.writeTextFileSync( 112 | resolve("build", folder, "index.html"), 113 | await render(magic, folder, plugins), 114 | ); 115 | } 116 | 117 | esbuild.stop(); 118 | 119 | if (config.base) { 120 | await screenshot(); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/utils.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | denoPlugins, 3 | esbuild, 4 | existsSync, 5 | parseJsonc, 6 | parseToml, 7 | parseYaml, 8 | rehypeStringify, 9 | remarkGfm, 10 | remarkParse, 11 | remarkRehype, 12 | resolve, 13 | toFileUrl, 14 | unified, 15 | } from "../deps.ts"; 16 | import { rehypeStarryNight } from "./lib/rehype_starry_night.ts"; 17 | import type { 18 | Config, 19 | FileTypes, 20 | JSX, 21 | Plugin, 22 | PluginResult, 23 | } from "./lib/types.ts"; 24 | 25 | /** 26 | * Read a file and return the file type and the file content 27 | */ 28 | export function readFileSync(...options: string[]): [FileTypes, string] { 29 | if (options.length === 1) { 30 | return [ 31 | options[0].split(".").pop() as FileTypes, 32 | Deno.readTextFileSync(options[0]), 33 | ]; 34 | } 35 | try { 36 | return [ 37 | options[0].split(".").pop() as FileTypes, 38 | Deno.readTextFileSync(options[0]), 39 | ]; 40 | } catch { 41 | options.shift(); 42 | return readFileSync(...options); 43 | } 44 | } 45 | 46 | export function readConfig(): Config { 47 | let extension; 48 | if (existsSync("pyro.yml")) extension = "yml"; 49 | if (existsSync("pyro.yaml")) extension = "yaml"; 50 | if (existsSync("pyro.json")) extension = "json"; 51 | if (existsSync("pyro.jsonc")) extension = "jsonc"; 52 | if (existsSync("pyro.toml")) extension = "toml"; 53 | 54 | const file = Deno.readTextFileSync(`pyro.${extension}`); 55 | 56 | if (extension === "yml" || extension === "yaml") { 57 | return parseYaml(file) as Config; 58 | } else if (extension === "json") { 59 | return JSON.parse(file); 60 | } else if (extension === "jsonc") { 61 | return parseJsonc(file) as unknown as Config; 62 | } else if (extension === "toml") { 63 | return parseToml(file) as unknown as Config; 64 | } 65 | throw new Error("No Pyro configuration file found. Try making a `pyro.yml`"); 66 | } 67 | 68 | function removeFrontmatter(markdown: string) { 69 | return markdown.replace(/^---.+?---/s, ""); 70 | } 71 | 72 | /** 73 | * Render Markdown to HTML 74 | */ 75 | export async function renderMD(data: string) { 76 | const file = await unified() 77 | .use(remarkParse) 78 | .use(remarkGfm) 79 | .use(remarkRehype, { allowDangerousHtml: true }) 80 | .use(rehypeStarryNight) 81 | .use(rehypeStringify, { allowDangerousHtml: true }) 82 | .process(removeFrontmatter(data)); 83 | 84 | return String(file); 85 | } 86 | 87 | /** 88 | * Loads all the provided plugins 89 | */ 90 | export function loadPlugins(plugins: string[]) { 91 | return Promise.all( 92 | plugins.map(async (plugin) => ((await import(plugin)).default as Plugin)()), 93 | ); 94 | } 95 | 96 | /** 97 | * Get the header elements from the config and plugins 98 | */ 99 | export function getHeaderElements(config: Config, plugins: PluginResult[]) { 100 | const header: { 101 | left: JSX.Element[]; 102 | right: JSX.Element[]; 103 | } = { 104 | left: config.header?.left?.map((value) => ( 105 | 109 | {value.split(" ").slice(0, -1).join(" ")} 110 | 111 | )) ?? [], 112 | right: config.header?.right?.map((value) => ( 113 | 117 | {value.split(" ").slice(0, -1).join(" ")} 118 | 119 | )) ?? [], 120 | }; 121 | 122 | for (const plugin of plugins) { 123 | if (!plugin.header) continue; 124 | if (plugin.header.left) { 125 | header.left = [...header.left, plugin.header.left]; 126 | } 127 | if (plugin.header.right) { 128 | header.right = [...header.right, plugin.header.right]; 129 | } 130 | } 131 | 132 | return header; 133 | } 134 | 135 | export async function importBuild(entrypoint: string) { 136 | entrypoint = resolve(entrypoint); 137 | 138 | let configPath: string | undefined = undefined; 139 | 140 | if (existsSync("./deno.json")) { 141 | configPath = resolve("./deno.json"); 142 | } else if (existsSync("./deno.jsonc")) { 143 | configPath = resolve("./deno.jsonc"); 144 | } 145 | 146 | const result = await esbuild.build({ 147 | plugins: [...denoPlugins({ configPath })], 148 | entryPoints: [toFileUrl(entrypoint).href], 149 | bundle: true, 150 | write: false, 151 | format: "esm", 152 | jsxImportSource: "https://esm.sh/preact@10.19.3", 153 | jsx: "automatic", 154 | }); 155 | const file = new TextDecoder().decode(result.outputFiles[0].contents); 156 | 157 | return await import( 158 | "data:text/javascript;base64," + btoa(file) 159 | ); 160 | } 161 | -------------------------------------------------------------------------------- /www/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import IconArrowRight from "https://deno.land/x/tabler_icons_tsx@0.0.4/tsx/arrow-right.tsx"; 2 | import { type PageProps } from "../../page.ts"; 3 | 4 | export const config = { 5 | title: "Home", 6 | description: 7 | "Pyro was designed from the ground up to be no-config and incredibly fast.", 8 | }; 9 | 10 | export default function Page(props: PageProps) { 11 | return ( 12 |
13 | {props.header} 14 |
15 |
16 |

17 | The{" "} 18 | SSG documentation site framework 19 | {" "} 20 | you've been waiting for. 21 |

22 | 42 |
43 | 46 |
47 |
48 |

Rundown

49 |
    50 |
  • 51 | ⚡️{" "} 52 | 53 | Pyro will help you ship a{" "} 54 | beautiful documentation site in no time. 55 | 56 |
  • 57 |
  • 58 | 💸{" "} 59 | 60 | Building a custom tech stack is expensive. Instead,{" "} 61 | focus on your content and just write Markdown files. 62 | 63 |
  • 64 |
  • 65 | 💥{" "} 66 | 67 | Ready for more? Advanced features{" "} 68 | like versioning, i18n, search and theme customizations are 69 | built-in with zero config required. 70 | 71 |
  • 72 |
  • 73 | 🧐{" "} 74 | 75 | Pyro at its core is a{" "} 76 | static-site generator. That means it can be deployed{" "} 77 | anywhere. 78 | 79 |
  • 80 |
81 | 82 |

Getting started

83 | 84 |

85 | Understand Pyro in 5 minutes by trying it out! 86 |

87 | 88 |

First install Pyro

89 | 90 | 91 | deno run -Ar https://deno.land/x/pyro/install.ts 92 | 93 | 94 |

Create the site

95 | 96 | 97 | pyro gen my-site 98 | 99 | 100 |

Start the dev server

101 | 102 | 103 | cd my-site && pyro dev 104 | 105 | 106 |

107 | Open{" "} 108 | 109 | http://localhost:8000 110 | {" "} 111 | and create your first site! 112 |

113 | 114 |

Features

115 | 116 |

Pyro was built from the ground up to maximize DX.

117 | 118 |
    119 |
  • 120 | Built with Deno, Preact, and Typescript 121 |
    122 | Stop using antiquated technology in your stack. Embrace the new! 123 |
  • 124 |
  • 125 | Created for developer experience first 126 |
    127 | Stop using tools that make you miserable. Life is too short. 128 |
  • 129 |
  • 130 | No configuration required 131 |
    132 | Pyro has fantastic defaults, and uses a lot of magic to make sure 133 | that your experience is as smooth as possible. No more hunting down 134 | plugins! 135 |
  • 136 |
137 |
138 | {props.footer} 139 |
140 | ); 141 | } 142 | -------------------------------------------------------------------------------- /src/lib/page.tsx: -------------------------------------------------------------------------------- 1 | import type { Config, FileTypes, JSX, Magic, RouteMap } from "./types.ts"; 2 | import { ExternalLink, Github, IconMenu2, posix } from "../../deps.ts"; 3 | import { renderMD } from "../utils.tsx"; 4 | import { Sidebar } from "./sidebar.tsx"; 5 | 6 | export function Header(props: { 7 | title: string; 8 | header: { 9 | left: JSX.Element[]; 10 | right: JSX.Element[]; 11 | }; 12 | github?: string; 13 | }) { 14 | return ( 15 |
16 |
17 | 18 | 19 | 20 |
21 | 25 |
26 |
27 | 28 |

29 | 30 | {props.title} 31 |

32 |
33 | {props.header.left} 34 |
35 | {props.header.right} 36 | {props.github && ( 37 | 42 | 43 | 44 | )} 45 |
46 |
47 | ); 48 | } 49 | 50 | export function Footer( 51 | props: { 52 | copyright?: string; 53 | footer: Record; 54 | github?: string; 55 | }, 56 | ) { 57 | return ( 58 | 103 | ); 104 | } 105 | 106 | export async function page(props: { 107 | page: { 108 | title: string; 109 | description: string; 110 | hide_navbar?: boolean; 111 | }; 112 | options: { 113 | markdown: string; 114 | route: string; 115 | config: Config; 116 | magic: Magic; 117 | file_type: FileTypes; 118 | dev: boolean; 119 | header: { 120 | left: JSX.Element[]; 121 | right: JSX.Element[]; 122 | }; 123 | } | { 124 | route: string; 125 | config: Config; 126 | body: string; 127 | dev: boolean; 128 | magic: Magic; 129 | }; 130 | }) { 131 | const hide_navbar = props.page.hide_navbar ?? 132 | props.options.config.hide_navbar ?? false; 133 | 134 | return ( 135 | 136 | 137 | {props.page.title} | {props.options.config.title} 138 | 139 | 140 | 141 | 142 | 146 | 147 | {props.options.config.base && !props.options.dev && ( 148 | 155 | )} 156 | 157 | 158 | {props.options.dev &&