├── .gitignore ├── .vscode └── settings.json ├── testdata ├── cat.png ├── posts │ ├── first │ │ └── hello.png │ ├── second │ │ └── hello2.png │ ├── 中文.md │ ├── fourth.md │ ├── sixth.md │ ├── seventh.md │ ├── second.md │ ├── fifth.md │ ├── first.md │ └── third.md ├── customRootDir │ ├── cat_custom_path.png │ └── posts │ │ └── custom.md └── my_blog.ts ├── .github ├── preview.png └── workflows │ └── ci.yml ├── deno.jsonc ├── LICENSE ├── deps.ts ├── types.d.ts ├── README.md ├── init.ts ├── blog_test.ts ├── blog.tsx └── components.tsx /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true 4 | } 5 | -------------------------------------------------------------------------------- /testdata/cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/denoland/deno_blog/main/testdata/cat.png -------------------------------------------------------------------------------- /.github/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/denoland/deno_blog/main/.github/preview.png -------------------------------------------------------------------------------- /testdata/posts/first/hello.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/denoland/deno_blog/main/testdata/posts/first/hello.png -------------------------------------------------------------------------------- /testdata/posts/second/hello2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/denoland/deno_blog/main/testdata/posts/second/hello2.png -------------------------------------------------------------------------------- /testdata/customRootDir/cat_custom_path.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/denoland/deno_blog/main/testdata/customRootDir/cat_custom_path.png -------------------------------------------------------------------------------- /testdata/posts/中文.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 中文 3 | author: 自定义中文名 4 | publish_date: 2022-08-19 5 | abstract: 这是一篇中文路径文章。 6 | --- 7 | 8 | 你好,世界! 9 | -------------------------------------------------------------------------------- /deno.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "lock": false, 3 | "tasks": { 4 | "demo": "deno run --allow-net --allow-read --allow-env=NODE_DEBUG --watch --no-check testdata/my_blog.ts --dev", 5 | "test": "deno test --no-check=remote --allow-read" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /testdata/posts/fourth.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Fourth post 3 | publish_date: 2023-01-30 4 | abstract: Image deno_blog but with unsantized HTML... 5 | disable_html_sanitization: true 6 | --- 7 | 8 | ## Cool Button Demo 9 | 10 | 11 | -------------------------------------------------------------------------------- /testdata/customRootDir/posts/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Custom post 3 | publish_date: 2022-03-20 4 | abstract: This is the first post with a custom root directory. 5 | --- 6 | 7 | It was popularised in the 1960s with the release of Letraset sheets containing 8 | Lorem Ipsum passages, and more recently with desktop publishing software like 9 | Aldus PageMaker including versions of Lorem Ipsum. 10 | -------------------------------------------------------------------------------- /testdata/my_blog.ts: -------------------------------------------------------------------------------- 1 | import blog from "../blog.tsx"; 2 | 3 | blog({ 4 | author: "Dino", 5 | title: "My Blog", 6 | description: "The blog description.", 7 | avatar: "https://deno-avatar.deno.dev/avatar/blog.svg", 8 | avatarClass: "rounded-full", 9 | links: [ 10 | { title: "bot@deno.com", url: "mailto:bot@deno.com" }, 11 | { title: "GitHub", url: "https://github.com/denobot" }, 12 | { title: "Twitter", url: "https://twitter.com/denobot" }, 13 | ], 14 | }); 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | check: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Clone repository 9 | uses: actions/checkout@v2 10 | 11 | - name: Install Deno 12 | uses: denoland/setup-deno@v1 13 | 14 | - name: Check formatting 15 | run: deno fmt --check 16 | 17 | - name: Lint 18 | run: deno lint 19 | 20 | - name: Tests 21 | run: deno task test 22 | -------------------------------------------------------------------------------- /testdata/posts/sixth.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Sixth post 3 | author: CUSTOM AUTHOR NAME 4 | publish_date: 2023-08-17 5 | snippet: Some snippet appears in main page. 6 | abstract: This is the sixth post showcasing math rendering 7 | summary: Sort summary 8 | description: A description to this post 9 | tags: ["sample", "tags"] 10 | allow_iframes: false 11 | disable_html_sanitization: false 12 | render_math: true 13 | --- 14 | 15 | # Tags 16 | 17 | - Tags make it easier for readers to find the desired article. 18 | 19 | ```markdown 20 | --- 21 | tags: ["deno", "tags"] 22 | --- 23 | ``` 24 | 25 | or 26 | 27 | ```markdown 28 | --- 29 | tags: 30 | - deno 31 | - tags 32 | --- 33 | ``` 34 | -------------------------------------------------------------------------------- /testdata/posts/seventh.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Seventh post 3 | pathname: "/uses-pathname" 4 | publish_date: 2022-05-02 5 | abstract: This is the seventh post. 6 | --- 7 | 8 | Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem 9 | Ipsum has been the industry's standard dummy text ever since the 1500s, when an 10 | unknown printer took a galley of type and scrambled it to make a type specimen 11 | book. It has survived not only five centuries, but also the leap into electronic 12 | typesetting, remaining essentially unchanged. It was popularised in the 1960s 13 | with the release of Letraset sheets containing Lorem Ipsum passages, and more 14 | recently with desktop publishing software like Aldus PageMaker including 15 | versions of Lorem Ipsum. 16 | -------------------------------------------------------------------------------- /testdata/posts/second.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Second post 3 | author: CUSTOM AUTHOR NAME 4 | publish_date: 2022-05-02 5 | abstract: This is the second post. 6 | --- 7 | 8 | 9 | 10 | Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem 11 | Ipsum has been the industry's standard dummy text ever since the 1500s, when an 12 | unknown printer took a galley of type and scrambled it to make a type specimen 13 | book. It has survived not only five centuries, but also the leap into electronic 14 | typesetting, remaining essentially unchanged. It was popularised in the 1960s 15 | with the release of Letraset sheets containing Lorem Ipsum passages, and more 16 | recently with desktop publishing software like Aldus PageMaker including 17 | versions of Lorem Ipsum. 18 | -------------------------------------------------------------------------------- /testdata/posts/fifth.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Fifth post 3 | author: CUSTOM AUTHOR NAME 4 | publish_date: 2023-02-17 5 | abstract: This is the fifth post showcasing math rendering 6 | render_math: true 7 | --- 8 | 9 | # Math rendering 10 | 11 | This post showcases math rendering using [KaTeX](https://katex.org/) in 12 | [deno-gfm](https://github.com/denoland/deno-gfm) 13 | 14 | ## Inline math 15 | 16 | Inline math is rendered using `$` delimiters, e.g. `$\sqrt{3x-1}+(1+x)^2$` 17 | renders as $\sqrt{3x-1}+(1+x)^2$ . 18 | 19 | And the letter `i` is rendered as $i$. 20 | 21 | ## Block math 22 | 23 | Block math is rendered using `$$` delimiters, e.g. `$$\sqrt{3x-1}+(1+x)^2$$` 24 | renders as 25 | 26 | $$ \sqrt{3x-1}+(1+x)^2 $$ 27 | 28 | ## Math in code blocks 29 | 30 | Math can be rendered in code blocks by using the `math` language tag, e.g. 31 | `math \sqrt{3x-1}+(1+x)^2` renders as 32 | 33 | ```math 34 | \sqrt{3x-1}+(1+x)^2 35 | ``` 36 | 37 | 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2018-2022 the Deno authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /deps.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 the Deno authors. All rights reserved. MIT license. 2 | 3 | export { serveDir } from "https://deno.land/std@0.193.0/http/file_server.ts"; 4 | export { walk } from "https://deno.land/std@0.193.0/fs/walk.ts"; 5 | export { 6 | dirname, 7 | fromFileUrl, 8 | join, 9 | relative, 10 | } from "https://deno.land/std@0.193.0/path/mod.ts"; 11 | export { 12 | type ConnInfo, 13 | serve, 14 | } from "https://deno.land/std@0.193.0/http/mod.ts"; 15 | export { extract as frontMatter } from "https://deno.land/std@0.193.0/front_matter/any.ts"; 16 | 17 | export * as gfm from "jsr:@deno/gfm@0.10.0"; 18 | export { Fragment, h } from "https://deno.land/x/htm@0.1.3/mod.ts"; 19 | export { 20 | default as html, 21 | type HtmlOptions, 22 | type VNode, 23 | } from "https://deno.land/x/htm@0.1.3/html.tsx"; 24 | import UnoCSS from "https://deno.land/x/htm@0.1.3/plugins/unocss.ts"; 25 | import ColorScheme from "https://deno.land/x/htm@0.1.3/plugins/color-scheme.ts"; 26 | 27 | export { 28 | createReporter, 29 | type Reporter as GaReporter, 30 | } from "https://deno.land/x/g_a@0.1.2/mod.ts"; 31 | export { default as callsites } from "https://raw.githubusercontent.com/kt3k/callsites/v1.0.0/mod.ts"; 32 | export { Feed, type Item as FeedItem } from "https://esm.sh/feed@4.2.2"; 33 | export { default as removeMarkdown } from "https://esm.sh/remove-markdown@0.5.0"; 34 | 35 | // Add syntax highlighting support for C by default 36 | import "https://esm.sh/prismjs@1.29.0/components/prism-c?no-check"; 37 | 38 | export { ColorScheme, UnoCSS }; 39 | export type UnoConfig = typeof UnoCSS extends ( 40 | arg: infer P | undefined, 41 | ) => unknown ? P 42 | : never; 43 | -------------------------------------------------------------------------------- /testdata/posts/first.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: First post 3 | publish_date: 2022-03-20 4 | abstract: This is the first post. 5 | cover_html: 6 | --- 7 | 8 | It was popularised in the 1960s with the release of Letraset sheets containing 9 | Lorem Ipsum passages, and more recently with desktop publishing software like 10 | Aldus PageMaker including versions of Lorem Ipsum. 11 | 12 | 13 | 14 | Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem 15 | Ipsum has been the industry's standard dummy text ever since the 1500s, when an 16 | unknown printer took a galley of type and scrambled it to make a type specimen 17 | book. It has survived not only five centuries, but also the leap into electronic 18 | typesetting, remaining essentially unchanged. It was popularised in the 1960s 19 | with the release of Letraset sheets containing Lorem Ipsum passages, and more 20 | recently with desktop publishing software like Aldus PageMaker including 21 | versions of Lorem Ipsum. 22 | 23 | ## Usage 24 | 25 | ```js 26 | import blog from "https://deno.land/x/blog/blog.tsx"; 27 | 28 | blog({ 29 | author: "Dino", 30 | title: "My Blog", 31 | description: "The blog description.", 32 | avatar: "https://deno-avatar.deno.dev/avatar/blog.svg", 33 | avatarClass: "rounded-full", 34 | links: [ 35 | { title: "Email", url: "mailto:bot@deno.com" }, 36 | { title: "GitHub", url: "https://github.com/denobot" }, 37 | { title: "Twitter", url: "https://twitter.com/denobot" }, 38 | ], 39 | }); 40 | ``` 41 | 42 | $100, $200, $300, $400, $500 43 | -------------------------------------------------------------------------------- /testdata/posts/third.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Third post 3 | author: CUSTOM AUTHOR NAME 4 | publish_date: 2022-08-19 5 | abstract: This is the third post. 6 | allow_iframes: true 7 | --- 8 | 9 | 10 | 11 | Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem 12 | Ipsum has been the industry's standard dummy text ever since the 1500s, when an 13 | unknown printer took a galley of type and scrambled it to make a type specimen 14 | book. It has survived not only five centuries, but also the leap into electronic 15 | typesetting, remaining essentially unchanged. It was popularised in the 1960s 16 | with the release of Letraset sheets containing Lorem Ipsum passages, and more 17 | recently with desktop publishing software like Aldus PageMaker including 18 | versions of Lorem Ipsum. 19 | 20 | Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem 21 | Ipsum has been the industry's standard dummy text ever since the 1500s, when an 22 | unknown printer took a galley of type and scrambled it to make a type specimen 23 | book. It has survived not only five centuries, but also the leap into electronic 24 | typesetting, remaining essentially unchanged. It was popularised in the 1960s 25 | with the release of Letraset sheets containing Lorem Ipsum passages, and more 26 | recently with desktop publishing software like Aldus PageMaker including 27 | versions of Lorem Ipsum. 28 | 29 | Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem 30 | Ipsum has been the industry's standard dummy text ever since the 1500s, when an 31 | unknown printer took a galley of type and scrambled it to make a type specimen 32 | book. It has survived not only five centuries, but also the leap into electronic 33 | typesetting, remaining essentially unchanged. It was popularised in the 1960s 34 | with the release of Letraset sheets containing Lorem Ipsum passages, and more 35 | recently with desktop publishing software like Aldus PageMaker including 36 | versions of Lorem Ipsum. 37 | 38 | Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem 39 | Ipsum has been the industry's standard dummy text ever since the 1500s, when an 40 | unknown printer took a galley of type and scrambled it to make a type specimen 41 | book. It has survived not only five centuries, but also the leap into electronic 42 | typesetting, remaining essentially unchanged. It was popularised in the 1960s 43 | with the release of Letraset sheets containing Lorem Ipsum passages, and more 44 | recently with desktop publishing software like Aldus PageMaker including 45 | versions of Lorem Ipsum. 46 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 the Deno authors. All rights reserved. MIT license. 2 | 3 | import type { ConnInfo, UnoConfig, VNode } from "./deps.ts"; 4 | 5 | export interface BlogContext { 6 | state: BlogState; 7 | connInfo: ConnInfo; 8 | next: () => Promise; 9 | } 10 | 11 | export interface BlogMiddleware { 12 | (req: Request, ctx: BlogContext): Promise; 13 | } 14 | 15 | type DateFormat = (date: Date) => string; 16 | 17 | export interface BlogSettings { 18 | /** The blog title */ 19 | title?: string; 20 | /** The blog description */ 21 | description?: string; 22 | /** URL to avatar. Can be relative. */ 23 | avatar?: string; 24 | /** CSS classes to use with the avatar. */ 25 | avatarClass?: string; 26 | /** URL to background cover. Can be relative. */ 27 | cover?: string; 28 | /** Color of the text that goes on the background cover. */ 29 | coverTextColor?: string; 30 | /** The author of the blog. Can be overridden by respective post settings. */ 31 | author?: string; 32 | /** Social links */ 33 | links?: { 34 | /** The link title */ 35 | title: string; 36 | /** The link */ 37 | url: string; 38 | /** The element to use as the icon of the link */ 39 | icon?: VNode; 40 | /** The link target */ 41 | target?: "_self" | "_blank" | "_parent" | "_top"; 42 | }[]; 43 | /** The element ot use as header */ 44 | header?: VNode; 45 | /** Whether to show the header on post pages */ 46 | showHeaderOnPostPage?: boolean; 47 | /** The element to use as section. Access to Post props. */ 48 | section?: (post: Post) => VNode; 49 | /** The element to use as footer */ 50 | footer?: VNode; 51 | /** Custom CSS */ 52 | style?: string; 53 | /** URL to open graph image. Can be relative. */ 54 | ogImage?: string | { 55 | url: string; 56 | twitterCard: "summary" | "summary_large_image" | "app" | "player"; 57 | }; 58 | /** Functions that are called before rendering and can modify the content or make other changes. */ 59 | middlewares?: BlogMiddleware[]; 60 | /** The ISO code of the language the blog is in */ 61 | lang?: string; 62 | /** Date appearance */ 63 | dateFormat?: DateFormat; 64 | /** The canonical URL of the blog */ 65 | canonicalUrl?: string; 66 | /** UnoCSS configuration */ 67 | unocss?: UnoConfig; 68 | /** Color scheme */ 69 | theme?: "dark" | "light" | "auto"; 70 | /** 71 | * URL to favicon. Can be relative. 72 | * Supports dark and light mode variants through "prefers-color-scheme". 73 | */ 74 | favicon?: string | { light?: string; dark?: string }; 75 | /** The port to serve the blog on */ 76 | port?: number; 77 | /** The hostname to serve the blog on */ 78 | hostname?: string; 79 | /** Whether to display readtime or not */ 80 | readtime?: boolean; 81 | /** The root directory of the blog contents */ 82 | rootDirectory?: string; 83 | } 84 | 85 | export interface BlogState extends BlogSettings { 86 | directory: string; 87 | } 88 | 89 | /** Represents a Post in the Blog. */ 90 | export interface Post { 91 | pathname: string; 92 | markdown: string; 93 | title: string; 94 | publishDate: Date; 95 | author?: string; 96 | snippet?: string; 97 | coverHtml?: string; 98 | /** An image URL which is used in the OpenGraph og:image tag. */ 99 | ogImage?: string; 100 | tags?: string[]; 101 | allowIframes?: boolean; 102 | disableHtmlSanitization?: boolean; 103 | readTime: number; 104 | renderMath?: boolean; 105 | } 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blog 2 | 3 | Minimal boilerplate blogging. All you need is one boilerplate JavaScript file 4 | that has 2 lines of code: 5 | 6 | ```js 7 | import blog from "https://deno.land/x/blog/blog.tsx"; 8 | 9 | blog(); 10 | ``` 11 | 12 | ## Getting started 13 | 14 | To initialize your own blog you can run following script: 15 | 16 | ```shellsession 17 | $ deno run -r --allow-read --allow-write https://deno.land/x/blog/init.ts ./directory/for/blog/ 18 | ``` 19 | 20 | _This command will setup a blog with a "Hello world" post so you can start 21 | writing right away._ 22 | 23 | Start local server with live reload: 24 | 25 | ```shellsession 26 | $ deno task dev 27 | ``` 28 | 29 | To ensure the best development experience, make sure to follow 30 | [Set up your environment](https://deno.land/manual/getting_started/setup_your_environment) 31 | from the Deno Manual. 32 | 33 | ## Configuration 34 | 35 | You can customize your blog as follows: 36 | 37 | ```js 38 | import blog, { ga, redirects } from "https://deno.land/x/blog/blog.tsx"; 39 | import { unocss_opts } from "./unocss.ts"; 40 | 41 | blog({ 42 | author: "Dino", 43 | title: "My Blog", 44 | description: "The blog description.", 45 | avatar: "avatar.png", 46 | avatarClass: "rounded-full", 47 | links: [ 48 | { title: "Email", url: "mailto:bot@deno.com" }, 49 | { title: "GitHub", url: "https://github.com/denobot" }, 50 | { title: "Twitter", url: "https://twitter.com/denobot" }, 51 | ], 52 | lang: "en", 53 | // localised format based on https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat 54 | dateFormat: (date) => 55 | new Intl.DateTimeFormat("en-GB", { dateStyle: "long" }).format(date), 56 | middlewares: [ 57 | ga("UA-XXXXXXXX-X"), 58 | redirects({ 59 | "/foo": "/my_post", 60 | // you can skip leading slashes too 61 | "bar": "my_post2", 62 | }), 63 | ], 64 | unocss: unocss_opts, // check https://github.com/unocss/unocss 65 | favicon: "favicon.ico", 66 | }); 67 | ``` 68 | 69 | ![Preview](./.github/preview.png) 70 | 71 | ## Customize the header and footer 72 | 73 | By default, we render the header and footer with builtin template using the blog 74 | settings. You can customize them as follows: 75 | 76 | ```jsx 77 | /** @jsx h */ 78 | 79 | import blog, { h } from "https://deno.land/x/blog/blog.tsx"; 80 | 81 | blog({ 82 | title: "My Blog", 83 | header:
Your custom header
, 84 | showHeaderOnPostPage: true, // by default, the header will only show on home, set showHeaderOnPostPage to true to make it show on each post page 85 | section: (post) => ( 86 |
Your custom section with access to Post props.
87 | ), 88 | footer:
Your custom footer
, 89 | }); 90 | ``` 91 | 92 | Beware to use `.tsx` extension to this extent. 93 | 94 | ## Hosting with Deno Deploy 95 | 96 | To deploy the project to the live internet, you can use 97 | [Deno Deploy](https://deno.com/deploy): 98 | 99 | 1. Push your project to GitHub. 100 | 2. [Create a Deno Deploy project](https://dash.deno.com/new). 101 | 3. [Link](https://deno.com/deploy/docs/projects#enabling) the Deno Deploy 102 | project to the `main.tsx` file in the root of the created repository. 103 | 4. The project will be deployed to a public `$project.deno.dev` subdomain. 104 | 105 | ## Self hosting 106 | 107 | You can also self-host the blog, in such case run: 108 | 109 | ```shellsession 110 | $ deno task serve 111 | ``` 112 | -------------------------------------------------------------------------------- /init.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 the Deno authors. All rights reserved. MIT license. 2 | 3 | import { join, resolve } from "https://deno.land/std@0.193.0/path/mod.ts"; 4 | 5 | const HELP = `deno_blog 6 | 7 | Initialize a new blog project. This will create all the necessary files for 8 | a new blog. 9 | 10 | To generate a blog in the './my_blog' subdirectory: 11 | deno run ${import.meta.url} ./my_blog 12 | 13 | To generate a blog in the current directory: 14 | deno run ${import.meta.url} . 15 | 16 | Print this message: 17 | deno run ${import.meta.url} --help 18 | `; 19 | 20 | const CURRENT_DATE = new Date(); 21 | const CURRENT_DATE_STRING = CURRENT_DATE.toISOString().slice(0, 10); 22 | 23 | const FIRST_POST_CONTENTS = `--- 24 | title: Hello world! 25 | publish_date: ${CURRENT_DATE_STRING} 26 | --- 27 | 28 | This is my first blog post! 29 | `; 30 | 31 | const MAIN_NAME = "main.tsx"; 32 | const MAIN_CONTENTS = `/** @jsx h */ 33 | 34 | import blog, { ga, redirects, h } from "blog"; 35 | 36 | blog({ 37 | title: "My Blog", 38 | description: "This is my new blog.", 39 | // header:
Your custom header
, 40 | // section: (post: Post) =>
Your custom section with access to Post props.
, 41 | // footer:
Your custom footer
, 42 | avatar: "https://deno-avatar.deno.dev/avatar/blog.svg", 43 | avatarClass: "rounded-full", 44 | author: "An author", 45 | 46 | // middlewares: [ 47 | 48 | // If you want to set up Google Analytics, paste your GA key here. 49 | // ga("UA-XXXXXXXX-X"), 50 | 51 | // If you want to provide some redirections, you can specify them here, 52 | // pathname specified in a key will redirect to pathname in the value. 53 | // redirects({ 54 | // "/hello_world.html": "/hello_world", 55 | // }), 56 | 57 | // ] 58 | }); 59 | `; 60 | 61 | const DENO_JSONC_NAME = "deno.jsonc"; 62 | const DENO_JSONC_CONTENTS = `{ 63 | "tasks": { 64 | "dev": "deno run --allow-net --allow-read --allow-env --watch main.tsx --dev", 65 | "serve": "deno run --allow-net --allow-read --allow-env --no-check main.tsx" 66 | }, 67 | "imports": { 68 | "blog": "https://deno.land/x/blog@0.7.0/blog.tsx" 69 | } 70 | } 71 | `; 72 | 73 | async function init(directory: string) { 74 | directory = resolve(directory); 75 | 76 | console.log(`Initializing blog in ${directory}...`); 77 | try { 78 | const dir = [...Deno.readDirSync(directory)]; 79 | if (dir.length > 0) { 80 | const confirmed = confirm( 81 | "You are trying to initialize blog in an non-empty directory, do you want to continue?", 82 | ); 83 | if (!confirmed) { 84 | throw new Error("Directory is not empty, aborting."); 85 | } 86 | } 87 | } catch (err) { 88 | if (!(err instanceof Deno.errors.NotFound)) { 89 | throw err; 90 | } 91 | } 92 | 93 | await Deno.mkdir(join(directory, "posts"), { recursive: true }); 94 | await Deno.writeTextFile( 95 | join(directory, "posts/hello_world.md"), 96 | FIRST_POST_CONTENTS, 97 | ); 98 | await Deno.writeTextFile(join(directory, MAIN_NAME), MAIN_CONTENTS); 99 | await Deno.writeTextFile( 100 | join(directory, DENO_JSONC_NAME), 101 | DENO_JSONC_CONTENTS, 102 | ); 103 | 104 | console.log("Blog initialized, run `deno task dev` to get started."); 105 | } 106 | 107 | function printHelp() { 108 | console.log(HELP); 109 | Deno.exit(0); 110 | } 111 | 112 | if (import.meta.main) { 113 | if (Deno.args.includes("-h") || Deno.args.includes("--help")) { 114 | printHelp(); 115 | } 116 | 117 | const directory = Deno.args[0]; 118 | if (directory == null) { 119 | printHelp(); 120 | } 121 | 122 | await init(directory); 123 | } else { 124 | throw new Error("This module is meant to be executed as a CLI."); 125 | } 126 | -------------------------------------------------------------------------------- /blog_test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 the Deno authors. All rights reserved. MIT license. 2 | 3 | import { configureBlog, createBlogHandler, redirects } from "./blog.tsx"; 4 | import { 5 | assert, 6 | assertEquals, 7 | assertStringIncludes, 8 | } from "https://deno.land/std@0.193.0/testing/asserts.ts"; 9 | import { fromFileUrl, join } from "https://deno.land/std@0.193.0/path/mod.ts"; 10 | 11 | const BLOG_URL = new URL("./testdata/main.js", import.meta.url).href; 12 | const TESTDATA_PATH = fromFileUrl(new URL("./testdata/", import.meta.url)); 13 | const BLOG_SETTINGS = await configureBlog(BLOG_URL, false, { 14 | author: "The author", 15 | title: "Test blog", 16 | description: "This is some description.", 17 | lang: "en-GB", 18 | middlewares: [ 19 | redirects({ 20 | "/to_second": "second", 21 | "/to_second_with_slash": "/second", 22 | "/external_redirect": "https://example.com", 23 | "second.html": "second", 24 | }), 25 | ], 26 | readtime: true, 27 | }); 28 | const CONN_INFO = { 29 | localAddr: { 30 | transport: "tcp" as const, 31 | hostname: "0.0.0.0", 32 | port: 8000, 33 | }, 34 | remoteAddr: { 35 | transport: "tcp" as const, 36 | hostname: "0.0.0.0", 37 | port: 8001, 38 | }, 39 | }; 40 | 41 | const blogHandler = createBlogHandler(BLOG_SETTINGS); 42 | const testHandler = (req: Request): Response | Promise => { 43 | return blogHandler(req, CONN_INFO); 44 | }; 45 | 46 | Deno.test("index page", async () => { 47 | const resp = await testHandler(new Request("https://blog.deno.dev")); 48 | assert(resp); 49 | assertEquals(resp.status, 200); 50 | assertEquals(resp.headers.get("content-type"), "text/html; charset=utf-8"); 51 | const body = await resp.text(); 52 | assertStringIncludes(body, ``); 53 | assertStringIncludes( 54 | body, 55 | ``, 56 | ); 57 | assertStringIncludes(body, `Test blog`); 58 | assertStringIncludes(body, `This is some description.`); 59 | assertStringIncludes(body, `href="/first"`); 60 | assertStringIncludes(body, `href="/second"`); 61 | }); 62 | 63 | Deno.test("posts/ first", async () => { 64 | const resp = await testHandler(new Request("https://blog.deno.dev/first")); 65 | assert(resp); 66 | assertEquals(resp.status, 200); 67 | assertEquals(resp.headers.get("content-type"), "text/html; charset=utf-8"); 68 | const body = await resp.text(); 69 | assertStringIncludes(body, ``); 70 | assertStringIncludes( 71 | body, 72 | ``, 73 | ); 74 | assertStringIncludes(body, `First post`); 75 | assertStringIncludes(body, `The author`); 76 | assertStringIncludes(body, `