${title}
22 |${description}
23 |
24 | ${published}
25 | ${readtime} to read
26 |
├── .arc ├── .env.example ├── .github └── workflows │ ├── cla.yml │ └── deploy.yml ├── .gitignore ├── README.md ├── app ├── api │ ├── admin │ │ ├── index.mjs │ │ └── webmentions.mjs │ ├── h-card.json │ ├── h-card.json.example │ ├── index.mjs │ ├── login.mjs │ ├── logout.mjs │ ├── posts │ │ └── $$.mjs │ ├── rss.mjs │ └── webmention.mjs ├── blog │ └── posts │ │ ├── 2023-02-23-pale-blue-dot.md │ │ ├── 2023-02-28-we-choose.md │ │ ├── 2023-03-01-element-styling-reference.md │ │ └── 2023-03-08-how-to-use.md ├── elements │ ├── admin-webmentions.mjs │ ├── blog-card.mjs │ ├── blog-pagination-button.mjs │ ├── blog-pagination.mjs │ ├── blog-posts.mjs │ ├── h-card.mjs │ ├── my-h-card.mjs │ ├── site-container.mjs │ ├── site-footer.mjs │ ├── site-header.mjs │ ├── site-layout.mjs │ ├── submit-button.mjs │ ├── text-input.mjs │ └── webmentions-list.mjs ├── head.mjs ├── lib │ ├── hljs-line-wrapper.mjs │ └── markdown-class-mappings.mjs └── pages │ ├── admin │ └── index.html │ ├── index.mjs │ ├── login.html │ └── posts │ └── $$.mjs ├── jobs ├── events │ ├── check-webmention │ │ └── index.mjs │ ├── incoming-webmention │ │ └── index.mjs │ └── outgoing-webmention │ │ └── index.mjs └── scheduled │ └── check-rss │ └── index.mjs ├── license.md ├── package-lock.json ├── package.json ├── prefs.arc ├── public ├── css │ ├── a11y-dark.min.css │ └── global.css ├── favicon.svg └── images │ ├── pale-blue-dot.jpg │ ├── the-moon.jpg │ ├── theme-elegant-dark.png │ ├── theme-elegant-light.png │ ├── theme-minimal-dark.png │ └── theme-minimal-light.png ├── shared ├── posts.mjs └── webmentions.mjs ├── src └── plugins │ ├── create-post-metadata.js │ └── create-rss-feed.js ├── theme-elegant.json └── theme-minimal.json /.arc: -------------------------------------------------------------------------------- 1 | @app 2 | enhance-blog-template 3 | 4 | @static 5 | prune true 6 | 7 | @plugins 8 | architect/plugin-lambda-invoker 9 | enhance/arc-plugin-enhance 10 | enhance/arc-plugin-styles 11 | create-post-metadata 12 | create-rss-feed 13 | 14 | @events 15 | check-webmention 16 | src jobs/events/check-webmention 17 | incoming-webmention 18 | src jobs/events/incoming-webmention 19 | outgoing-webmention 20 | src jobs/events/outgoing-webmention 21 | 22 | @scheduled 23 | check-rss 24 | rate 1 day 25 | src jobs/scheduled/check-rss 26 | 27 | @enhance-styles 28 | config theme-minimal.json 29 | 30 | @aws 31 | runtime nodejs18.x 32 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # rename to ".env", change the sekret, and do NOT commit 2 | SECRET_PASSWORD=zerocool 3 | -------------------------------------------------------------------------------- /.github/workflows/cla.yml: -------------------------------------------------------------------------------- 1 | name: "CLA Assistant" 2 | on: 3 | issue_comment: 4 | types: [created] 5 | pull_request_target: 6 | types: [opened,closed,synchronize] 7 | 8 | permissions: 9 | actions: write 10 | contents: write 11 | pull-requests: write 12 | statuses: write 13 | 14 | jobs: 15 | CLAAssistant: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: "CLA Assistant" 19 | if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' 20 | uses: contributor-assistant/github-action@v2.4.0 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} 24 | with: 25 | path-to-signatures: 'signatures/v1/cla.json' 26 | path-to-document: 'https://github.com/enhance-dev/.github/blob/main/CLA.md' 27 | branch: 'main' 28 | allowlist: brianleroux,colepeters,kristoferjoseph,macdonst,ryanbethel,ryanblock,tbeseda 29 | remote-organization-name: enhance-dev 30 | remote-repository-name: .github 31 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | # Push tests pushes; PR tests merges 4 | on: [ push, pull_request ] 5 | 6 | defaults: 7 | run: 8 | shell: bash 9 | 10 | jobs: 11 | # Deploy the build 12 | deploy_staging: 13 | name: Deploy staging 14 | if: github.ref == 'refs/heads/main' && github.event_name == 'push' # Don't run twice for PRs (for now) 15 | runs-on: ubuntu-latest 16 | concurrency: 17 | group: staging_${{ github.repository }} 18 | 19 | steps: 20 | - name: Check out repo 21 | uses: actions/checkout@v3 22 | 23 | - name: Set up Node.js 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: 16 27 | 28 | - name: Install 29 | run: npm install 30 | 31 | - name: Create blog post metadata 32 | run: npm run posts 33 | 34 | - name: Create RSS feed 35 | run: npm run rss 36 | env: 37 | SITE_URL: ${{ secrets.SITE_URL_STAGING }} 38 | 39 | - name: Deploy to staging 40 | uses: beginner-corp/actions/deploy@main 41 | with: 42 | begin_token: ${{ secrets.BEGIN_TOKEN }} 43 | begin_env_name: staging 44 | channel: 'main' 45 | 46 | # Deploy the build 47 | deploy_production: 48 | name: Deploy production 49 | if: startsWith(github.ref, 'refs/tags/v') && github.event_name == 'push' # Don't run twice for PRs (for now) 50 | runs-on: ubuntu-latest 51 | concurrency: 52 | group: production_${{ github.repository }} 53 | 54 | steps: 55 | - name: Check out repo 56 | uses: actions/checkout@v3 57 | 58 | - name: Set up Node.js 59 | uses: actions/setup-node@v3 60 | with: 61 | node-version: 16 62 | 63 | - name: Install 64 | run: npm install 65 | 66 | - name: Create blog post metadata 67 | run: npm run posts 68 | 69 | - name: Create RSS feed 70 | run: npm run rss 71 | env: 72 | SITE_URL: ${{ secrets.SITE_URL_PRODUCTION }} 73 | 74 | - name: Deploy to production 75 | uses: beginner-corp/actions/deploy@main 76 | with: 77 | begin_token: ${{ secrets.BEGIN_TOKEN }} 78 | begin_env_name: production 79 | channel: 'main' 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env 3 | 4 | # Enhance temp files 5 | .enhance/ 6 | shared/enhance-styles 7 | 8 | # Generated assets 9 | public/static.json 10 | shared/static.json 11 | public/bundles/ 12 | public/pages/ 13 | 14 | # Architect CloudFormation 15 | sam.json 16 | sam.yaml 17 | 18 | .DS_Store 19 | # Logs 20 | logs 21 | *.log 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | lerna-debug.log* 26 | .pnpm-debug.log* 27 | 28 | # Diagnostic reports (https://nodejs.org/api/report.html) 29 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 30 | 31 | # Runtime data 32 | pids 33 | *.pid 34 | *.seed 35 | *.pid.lock 36 | 37 | # Directory for instrumented libs generated by jscoverage/JSCover 38 | lib-cov 39 | 40 | # Coverage directory used by tools like istanbul 41 | coverage 42 | *.lcov 43 | 44 | # nyc test coverage 45 | .nyc_output 46 | 47 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 48 | .grunt 49 | 50 | # Bower dependency directory (https://bower.io/) 51 | bower_components 52 | 53 | # node-waf configuration 54 | .lock-wscript 55 | 56 | # Compiled binary addons (https://nodejs.org/api/addons.html) 57 | build/Release 58 | 59 | # Dependency directories 60 | node_modules/ 61 | jspm_packages/ 62 | 63 | # Snowpack dependency directory (https://snowpack.dev/) 64 | web_modules/ 65 | 66 | # TypeScript cache 67 | *.tsbuildinfo 68 | 69 | # Optional npm cache directory 70 | .npm 71 | 72 | # Optional eslint cache 73 | .eslintcache 74 | 75 | # Optional stylelint cache 76 | .stylelintcache 77 | 78 | # Microbundle cache 79 | .rpt2_cache/ 80 | .rts2_cache_cjs/ 81 | .rts2_cache_es/ 82 | .rts2_cache_umd/ 83 | 84 | # Optional REPL history 85 | .node_repl_history 86 | 87 | # Output of 'npm pack' 88 | *.tgz 89 | 90 | # Yarn Integrity file 91 | .yarn-integrity 92 | 93 | # dotenv environment variable files 94 | .env 95 | .env.development.local 96 | .env.test.local 97 | .env.production.local 98 | .env.local 99 | 100 | # parcel-bundler cache (https://parceljs.org/) 101 | .cache 102 | .parcel-cache 103 | 104 | # Next.js build output 105 | .next 106 | out 107 | 108 | # Nuxt.js build / generate output 109 | .nuxt 110 | dist 111 | 112 | # Gatsby files 113 | .cache/ 114 | # Comment in the public line in if your project uses Gatsby and not Next.js 115 | # https://nextjs.org/blog/next-9-1#public-directory-support 116 | # public 117 | 118 | # vuepress build output 119 | .vuepress/dist 120 | 121 | # vuepress v2.x temp and cache directory 122 | .temp 123 | .cache 124 | 125 | # Docusaurus cache and generated files 126 | .docusaurus 127 | 128 | # Serverless directories 129 | .serverless/ 130 | 131 | # FuseBox cache 132 | .fusebox/ 133 | 134 | # DynamoDB Local files 135 | .dynamodb/ 136 | 137 | # TernJS port file 138 | .tern-port 139 | 140 | # Stores VSCode versions used for testing VSCode extensions 141 | .vscode-test 142 | 143 | # yarn v2 144 | .yarn/cache 145 | .yarn/unplugged 146 | .yarn/build-state.yml 147 | .yarn/install-state.gz 148 | .pnp.* 149 | 150 | # Ignore generated files 151 | app/api/posts.json 152 | app/api/rss.br 153 | app/api/rss.xml 154 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |  2 | 3 | # enhance-blog-template 4 | 5 | This is the repo containing the blog template project using Enhance. 6 | 7 | ``` 8 | app 9 | ├── api ..................... data routes 10 | │ ├── admin 11 | │ │ ├── index.mjs ....... load data for admin route 12 | │ │ └── webmentions.mjs . approve webmention 13 | │ ├── posts 14 | │ │ └── $$.mjs .......... load data for individual blog post 15 | │ ├── index.mjs ........... list of blog posts 16 | │ ├── login.mjs ........... verify login 17 | │ ├── logout.mjs .......... log out user 18 | │ ├── rss.mjs ............. rss feed 19 | │ └── webmention.mjs ...... incoming webmention 20 | ├── blog 21 | │ └── posts ............... post files in markdown format 22 | │ └── *.md 23 | ├── elements ................ custom element pure functions 24 | │ └── *.mjs 25 | ├── lib 26 | │ ├── hljs-line-wrapper.mjs 27 | │ └── markdown-class-mappings.mjs 28 | ├── pages ................... file-based routing 29 | │ ├── posts 30 | │ │ └── $$.mjs .......... individual blog post 31 | │ └── index.mjs ........... list of blog posts 32 | └── head.mjs ................ head tag for each page 33 | jobs 34 | ├── events 35 | │ ├── check-webmention .... check new rss item for a webmention 36 | │ ├── incoming-webmention . accept incoming webmention 37 | │ └── outgoing-webmention . send outgoing webmention 38 | └── scheduled ............... scheduled functions 39 | └── check-rss ........... look for new rss items 40 | shared ...................... code shared between app and jobs 41 | ├── posts.mjs ............... database methods for rss items 42 | └── webmentions ............. database methods for webmentions 43 | ``` 44 | 45 | ## Quick Start 46 | 47 | - Clone this repo: 48 | 49 | ```bash 50 | git clone git@github.com:enhance-dev/enhance-blog-template.git 51 | ``` 52 | 53 | - cd into the repo and do an npm install 54 | 55 | ```bash 56 | cd enhance-blog-template 57 | npm install 58 | ``` 59 | - Start the development server. 60 | 61 | ```bash 62 | npm start 63 | ``` 64 | - Open a browser tab to http://localhost:3333 65 | - Start editing your blog 66 | 67 | ## Deploy to Production 68 | 69 | > 🚨 Don't forget to run: `npm run posts` and `npm run rss` to generate the static JSON before deploying. 70 | 71 | - [Install](https://begin.com/docs/getting-started/installing-the-begin-cli) the Begin CLI. 72 | - Login to Begin 73 | 74 | ```bash 75 | begin login 76 | ``` 77 | 78 | - Create your application and staging environment by following the interactive prompts: 79 | 80 | ```bash 81 | begin create 82 | This project doesn't appear to be associated with a Begin app 83 | ? Would you like to create a Begin app based on this project? (Y/n) · true 84 | ? What would you like to name your app? · blog-template 85 | ? What would you like to name your first environment? · staging 86 | Archiving and uploading project to Begin... 87 | Project uploaded, you can now exit this process and check its status with: begin deploy --status 88 | Beginning deployment of 'staging' 89 | Packaging build for deployment 90 | Publishing build to Begin 91 | Build completed! 92 | Deployed 'staging' to: https://blog-template.begin.app 93 | ``` 94 | - [Optional] create a production environment. 95 | 96 | ```bash 97 | begin create --env production 98 | App environment 'production' created at https://blog-template-prod.begin.app 99 | ``` 100 | 101 | ## Styling 102 | 103 | This repo comes preloaded with two basic themes: 104 | 105 | | | Minimal | Elegant | 106 | | - | - | - | 107 | | light |  |  | 108 | | dark |  |  | 109 | 110 | To switch from one theme to another, change the filename referenced under the `@enhance-styles` pragma in the `.arc` file: 111 | 112 | ``` 113 | @enhance-styles 114 | config theme-minimal.json 115 | ``` 116 | 117 | These themes are intended as basic starting points for your own customization. To learn more about how to style your blog, [check out this deep dive on the Begin blog](https://begin.com/blog/posts/2023-04-06-customizing-the-enhance-blog-template)! 118 | 119 | ## Configuring CI/CD 120 | 121 | This repo comes with a GitHub action that will deploy our site to `staging` when there is a commit to the `main` branch and `production` when you tag a release. 122 | 123 | For this to work you must [create a repo secret](https://docs.github.com/en/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository) named `BEGIN_TOKEN`. Once you successfully login to Begin using the CLI command `begin login` you can retrieve the value for `BEGIN_TOKEN` in the file `~/.begin/config.json`. Use the value of `access_token` in this file as the value for `BEGIN_TOKEN`. 124 | 125 | Additionally to ensure you `/rss` feed points to the correct environment you will need to create two additional repo secrets. 126 | 127 | - `SITE_URL_STAGING`: set to the url you received when creating the `staging` environment 128 | - `SITE_URL_PRODUCTION`: set to the url you received when creating the `production` environment 129 | 130 | ## Setting up WebMentions 131 | 132 | > Webmention is a W3C recommendation that describes a simple protocol to notify any URL when a website links to it, and for web pages to request notifications when somebody links to them. 133 | 134 | The `enhance-blog-template` supports both incoming and outgoing webmentions. To enable this funcationality, complete the following steps: 135 | 136 | - Add your information to `app/api/h-card.json`. The template will add a `h-card` to each of your blog posts. This will allow remote sites to discover information about you when you link to them. Only include what information you feel comfortable in sharing. A good default would be name, photo and url. For example: 137 | 138 | ```json 139 | { 140 | "name": "Simon MacDonald", 141 | "photo": "https://github.com/macdonst.png", 142 | "url": "https://bookrecs.org" 143 | } 144 | ``` 145 | 146 | For a more detailed example see [app/api/h-card.json.example](./app/api/h-card.json.example). 147 | 148 | - Set a password for the `/admin` route: 149 | 150 | ```bash 151 | begin env create --env staging --name SECRET_PASSWORD --value yoursecretpassword 152 | ``` 153 | 154 | You can use the `/admin` route to approve incoming webmentions. They won't show up under your blog post until you approve them. 155 | -------------------------------------------------------------------------------- /app/api/admin/index.mjs: -------------------------------------------------------------------------------- 1 | import { getWebMentions } from '../../../shared/webmentions.mjs' 2 | 3 | /** @type {import('@enhance/types').EnhanceApiFn} */ 4 | export async function get({ session }) { 5 | const authorized = !!(session.authorized) 6 | if (!authorized) return { location: '/login' } 7 | 8 | const mentions = await getWebMentions() 9 | 10 | return { 11 | json: { authorized, mentions } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/api/admin/webmentions.mjs: -------------------------------------------------------------------------------- 1 | import { getWebMention, upsertWebMention } from '../../../shared/webmentions.mjs' 2 | 3 | /** @type {import('@enhance/types').EnhanceApiFn} */ 4 | export async function post({ session, body }) { 5 | const authorized = !!(session.authorized) 6 | if (!authorized) return { location: '/' } 7 | 8 | const { key, approved } = body 9 | const mention = await getWebMention(key) 10 | mention.approved = approved === 'true' 11 | 12 | await upsertWebMention(mention) 13 | 14 | return { location: '/admin' } 15 | } 16 | -------------------------------------------------------------------------------- /app/api/h-card.json: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /app/api/h-card.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "name":"Sally Ride", 3 | "honorificPrefix": "Dr.", 4 | "givenName": "Sally", 5 | "additionalName": "K.", 6 | "familyName": "Ride", 7 | "honorificSuffix": "Ph.D.", 8 | "nickname": "sallykride", 9 | "org": "Sally Ride Science", 10 | "photo": "http://example.com/sk.jpg", 11 | "url": "http://sally.example.com", 12 | "email": "mailto:sally@example.com", 13 | "tel": "+1.818.555.1212", 14 | "streetAddress": "123 Main st.", 15 | "locality": "Los Angeles", 16 | "region": "California", 17 | "postalCode": "91316", 18 | "countryName": "U.S.A", 19 | "bday": "1951-05-26", 20 | "category": "physicist", 21 | "note": "First American woman in space." 22 | } 23 | -------------------------------------------------------------------------------- /app/api/index.mjs: -------------------------------------------------------------------------------- 1 | import { dirname, join } from 'node:path' 2 | import url from 'node:url' 3 | import { readFileSync } from 'node:fs' 4 | 5 | /** @type {import('@enhance/types').EnhanceApiFn} */ 6 | export async function get(req) { 7 | const here = dirname(url.fileURLToPath(import.meta.url)) 8 | const base = join(here, 'posts.json') 9 | const posts = JSON.parse(readFileSync(base, 'utf-8')) 10 | .reverse() 11 | 12 | const hCardPath = join(here, 'h-card.json') 13 | const hCard = JSON.parse(readFileSync(hCardPath, 'utf-8')) 14 | 15 | const parsedLimit = parseInt(req.query.limit, 10) 16 | const limit = parsedLimit > 0 ? parsedLimit : 20 17 | const parsedOffset = parseInt(req.query.offset, 10) 18 | const offset = parsedOffset >= 0 ? parsedOffset : 0 19 | const total = posts.length 20 | 21 | return { 22 | json: { 23 | posts, 24 | limit, 25 | offset, 26 | total, 27 | hCard, 28 | }, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/api/login.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('@enhance/types').EnhanceApiFn} */ 2 | export async function post({ body }) { 3 | const authorized = body.password === process.env.SECRET_PASSWORD 4 | 5 | return { 6 | location: '/admin', 7 | session: { authorized } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/api/logout.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('@enhance/types').EnhanceApiFn} */ 2 | export async function get() { 3 | return { 4 | location: '/', 5 | session: { authorized: false } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/api/posts/$$.mjs: -------------------------------------------------------------------------------- 1 | import { dirname, join } from 'node:path' 2 | import url from 'node:url' 3 | import { readFileSync } from 'node:fs' 4 | import { URL } from 'node:url' 5 | import { Arcdown } from 'arcdown' 6 | import HljsLineWrapper from '../../lib/hljs-line-wrapper.mjs' 7 | import { default as defaultClassMapping } from '../../lib/markdown-class-mappings.mjs' 8 | import { getWebMentions } from '../../../shared/webmentions.mjs' 9 | 10 | /** @type {import('@enhance/types').EnhanceApiFn} */ 11 | export async function get(req) { 12 | 13 | // reinvoked each req so no weird regexp caching 14 | const arcdown = new Arcdown({ 15 | pluginOverrides: { 16 | markdownItToc: { 17 | containerClass: 'toc mb2 ml-2', 18 | listType: 'ul', 19 | }, 20 | markdownItClass: defaultClassMapping, 21 | }, 22 | hljs: { 23 | sublanguages: { javascript: ['xml', 'css'] }, 24 | plugins: [new HljsLineWrapper({ className: 'code-line' })], 25 | }, 26 | }) 27 | 28 | const { path: activePath } = req 29 | let docPath = activePath.replace(/^\/?blog\//, '') || 'index' 30 | if (docPath.endsWith('/')) { 31 | docPath += 'index' // trailing slash == index.md file 32 | } 33 | 34 | const docURL = new URL(`../../blog${docPath}.md`, import.meta.url) 35 | const filePath = process.platform !== 'win32' ? docURL.pathname : docURL.pathname.substring(1, docURL.pathname.length) 36 | 37 | let docMarkdown 38 | try { 39 | docMarkdown = readFileSync(filePath, 'utf-8') 40 | } catch (_err) { 41 | console.log(_err) 42 | return { statusCode: 404 } 43 | } 44 | const post = await arcdown.render(docMarkdown) 45 | const mentions = (await getWebMentions()).filter(mention => (mention.targetPath === activePath && mention.approved)) 46 | 47 | let here = dirname(url.fileURLToPath(import.meta.url)) 48 | let hCardPath = join(here, '..', 'h-card.json') 49 | let hCard = JSON.parse(readFileSync(hCardPath, 'utf-8')) 50 | 51 | return { 52 | json: { 53 | post, 54 | mentions, 55 | hCard 56 | }, 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/api/rss.mjs: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import url from 'node:url' 3 | import { readFileSync } from 'node:fs' 4 | 5 | export async function get(req) { 6 | let here = path.dirname(url.fileURLToPath(import.meta.url)) 7 | 8 | let acceptEncoding = (req.headers && req.headers['accept-encoding'] || 9 | req.headers && req.headers['Accept-Encoding']) 10 | let returnCompressed = acceptEncoding && acceptEncoding.includes('br') 11 | 12 | let resp = { 13 | statusCode: 200, 14 | headers: { 15 | 'content-type': 'application/rss+xml; charset=UTF-8', 16 | }, 17 | } 18 | 19 | if (returnCompressed) { 20 | let postsFilePath = path.join(here, 'rss.br') 21 | resp.body = readFileSync(postsFilePath, 'utf-8') 22 | resp.isBase64Encoded = true 23 | resp.headers['content-encoding'] = 'br' 24 | } else { 25 | let postsFilePath = path.join(here, 'rss.xml') 26 | resp.body = readFileSync(postsFilePath, 'utf-8') 27 | } 28 | 29 | return resp 30 | } 31 | -------------------------------------------------------------------------------- /app/api/webmention.mjs: -------------------------------------------------------------------------------- 1 | import arc from "@architect/functions" 2 | 3 | /** @type {import('@enhance/types').EnhanceApiFn} */ 4 | export async function post({ body }) { 5 | const { target, source } = body 6 | 7 | // validate incoming webmention 8 | const errors = [] 9 | if (!target) { 10 | errors.push('missing_target') 11 | } else if (!target.startsWith('http')) { 12 | errors.push('invalid_target') 13 | } 14 | if (!source) { 15 | errors.push('missing_source') 16 | } else if (!source.startsWith('http')) { 17 | errors.push('invalid_source') 18 | } 19 | if (target && source && target === source) { 20 | errors.push('invalid: source must not match target') 21 | } 22 | 23 | if (errors.length > 0) { 24 | console.log(`Discovered ${errors.length} errors, returning 400:`, errors) 25 | return { 26 | code: 400, 27 | json: { errors }, 28 | } 29 | } 30 | 31 | // publish event to store incoming webmention 32 | await arc.events.publish({ 33 | name: 'incoming-webmention', 34 | payload: { source, target }, 35 | }) 36 | 37 | return { 38 | code: 202, 39 | text: 'accepted', 40 | } 41 | } 42 | 43 | export async function get() { 44 | return { 45 | code: 405, 46 | text: 'plz send POST', 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /app/blog/posts/2023-02-23-pale-blue-dot.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Pale Blue Dot 3 | description: The Pale Blue Dot is a photograph of planet Earth taken in 1990 by the Voyager 1 spacecraft from a record distance of more than 6 billion kilometers (4 billion miles) from Earth. At Carl Sagan’s urging, the spacecraft, which was leaving the Solar System, was commanded by NASA to turn its camera around and to take a photograph of Earth across a great expanse of space. 4 | published: "February 23, 2023" 5 | --- 6 | 7 |  8 | 9 | Look again at that dot. That's here. That's home. That's us. On it everyone you love, everyone you know, everyone you ever heard of, every human being who ever was, lived out their lives. The aggregate of our joy and suffering, thousands of confident religions, ideologies, and economic doctrines, every hunter and forager, every hero and coward, every creator and destroyer of civilization, every king and peasant, every young couple in love, every mother and father, hopeful child, inventor and explorer, every teacher of morals, every corrupt politician, every "superstar," every "supreme leader," every saint and sinner in the history of our species lived there--on a mote of dust suspended in a sunbeam. 10 | 11 | 12 | The Earth is a very small stage in a vast cosmic arena. Think of the rivers of blood spilled by all those generals and emperors so that, in glory and triumph, they could become the momentary masters of a fraction of a dot. Think of the endless cruelties visited by the inhabitants of one corner of this pixel on the scarcely distinguishable inhabitants of some other corner, how frequent their misunderstandings, how eager they are to kill one another, how fervent their hatreds. 13 | 14 | 15 | Our posturings, our imagined self-importance, the delusion that we have some privileged position in the Universe, are challenged by this point of pale light. Our planet is a lonely speck in the great enveloping cosmic dark. In our obscurity, in all this vastness, there is no hint that help will come from elsewhere to save us from ourselves. 16 | 17 | 18 | The Earth is the only world known so far to harbor life. There is nowhere else, at least in the near future, to which our species could migrate. Visit, yes. Settle, not yet. Like it or not, for the moment the Earth is where we make our stand. 19 | 20 | 21 | It has been said that astronomy is a humbling and character-building experience. There is perhaps no better demonstration of the folly of human conceits than this distant image of our tiny world. To me, it underscores our responsibility to deal more kindly with one another, and to preserve and cherish the pale blue dot, the only home we've ever known. 22 | 23 | -------------------------------------------------------------------------------- /app/blog/posts/2023-02-28-we-choose.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "We Choose to go to the Moon" 3 | description: There is no strife, no prejudice, no national conflict in outer space as yet. Its hazards are hostile to us all. Its conquest deserves the best of all mankind, and its opportunity for peaceful cooperation many never come again. But why, some say, the moon? Why choose this as our goal? And they may well ask why climb the highest mountain? Why, 35 years ago, fly the Atlantic? Why does Rice play Texas? 4 | published: "February 28, 2023" 5 | --- 6 | 7 |  8 | 9 | There is no strife, no prejudice, no national conflict in outer space as yet. Its hazards are hostile to us all. Its conquest deserves the best of all mankind, and its opportunity for peaceful cooperation many never come again. But why, some say, the moon? Why choose this as our goal? And they may well ask why climb the highest mountain? Why, 35 years ago, fly the Atlantic? Why does Rice play Texas? 10 | 11 | 12 | We choose to go to the moon. We choose to go to the moon in this decade and do the other things, not because they are easy, but because they are hard, because that goal will serve to organize and measure the best of our energies and skills, because that challenge is one that we are willing to accept, one we are unwilling to postpone, and one which we intend to win, and the others, too. 13 | 14 | 15 | It is for these reasons that I regard the decision last year to shift our efforts in space from low to high gear as among the most important decisions that will be made during my incumbency in the office of the Presidency. 16 | 17 | 18 | In the last 24 hours we have seen facilities now being created for the greatest and most complex exploration in man's history. We have felt the ground shake and the air shattered by the testing of a Saturn C-1 booster rocket, many times as powerful as the Atlas which launched John Glenn, generating power equivalent to 10,000 automobiles with their accelerators on the floor. We have seen the site where the F-1 rocket engines, each one as powerful as all eight engines of the Saturn combined, will be clustered together to make the advanced Saturn missile, assembled in a new building to be built at Cape Canaveral as tall as a 48 story structure, as wide as a city block, and as long as two lengths of this field. 19 | -------------------------------------------------------------------------------- /app/blog/posts/2023-03-01-element-styling-reference.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Element Styling Reference 3 | description: "A post using a range of HTML elements which you can use as a styling reference." 4 | published: "March 1, 2023" 5 | --- 6 | 7 | This post uses a wide range of HTML elements, which you can use a styling reference. For example, this is a plain paragraph, and it features things like emphasized text and even text of strong importance. But perhaps you’d rather see things in an unordered list? 8 | 9 | - Here's a list item. 10 | - And a second. 11 | - And, finally, a third list item, which uses a fair bit more text, so that you can see what your list item looks like with line wrapping. 12 | 13 | Or perhaps you'd rather something a little more orderly: 14 | 15 | 1. This is an ordered list. 16 | 1. Its items are preceded by ordinal characters like numbers and letters. 17 | 1. Use this list when the order of the items is important. For example, the ingredients in a recipe would be written using an unordered list, but the cooking instructions would be written using an ordered list. Now this line wraps, too. 18 | 19 | For those times when you need to present a list of terms with accompanying details for each, you can use a description list: 20 | 21 |
51 |
52 |
53 |
54 |
55 | `
56 | }
57 | ```
58 |
59 | For information on how to change the code highlighting colours, see the post [How to use this template](/posts/2023-03-08-how-to-use). (Hey, there's a link!)
60 |
61 | #### A fourth level heading
62 |
63 | If you want to display tabular data, there is, of course, the `table` element! Here's one detailing some of the HTML elements used to build tables:
64 |
65 | | HTML Element | Common name | Role |
66 | |--------------|-------------|------|
67 | | `` | Table header cell | Define a cell within the header of the table | 70 | | ` | ` | Table body | Define the body of the table | 71 | | `||||||||||
---|---|---|---|---|---|---|---|---|---|---|
` | Table data cell | Define a cell in the table |
73 |
74 | * * *
75 |
76 | Sometimes it's useful to separate flow content with a horizontal rule, or ` ` element. Look just above this text to see one in action! 77 | 78 | ## That's it! 79 | 80 | If you think of any other elements you’d like to see showcased in this example post, feel free to [let us know on Discord](https://enhance.dev/discord), or [file an issue on GitHub!](https://github.com/enhance-dev/enhance-blog-template/issues) 81 | -------------------------------------------------------------------------------- /app/blog/posts/2023-03-08-how-to-use.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: How to use this template 3 | description: "Tips on how to customize this blog template." 4 | published: "March 8, 2023" 5 | --- 6 | 7 | While we've done our best to make this blog template usable right out of the box, we assume you will want to customize it to suit your own needs. The following is a set of spots you may want to update before deploying to production. 8 | 9 | ## Blog Posts 10 | 11 | You'll probably want to clear out all the files in `app/blog/posts` since you didn't write them. Any file with the extension `.md` will automatically be added to your blog. The filename should follow the format `YYYY-MM-DD-title.md` (e.g. `2023-03-07-new-post.md`). 12 | 13 | In order for the file to be correctly processed it needs to include some frontmatter. 14 | 15 | ```yaml 16 | --- 17 | title: The title 18 | description: A description of your post 19 | published: The date published in the format "Month Day, Year" (e.g. March 7, 2023) 20 | --- 21 | ``` 22 | 23 | Then the rest of your post can be any valid markdown including fenced code blocks. For more info on how to add additional languages to the syntax highlighting check out the documentation for [Arcdown](https://github.com/architect/arcdown). 24 | 25 | ## Styling 26 | 27 | Styles for this template are applied in the following places: 28 | 29 | 1. `styleguide.json`: Dark and light colours are defined here, as are the font stacks to be used on headings and body text. System font stacks are used by default; you can find some great alternatives over at [Modern Font Stacks](https://modernfontstacks.com/). 30 | 1. `public/css/global.css`: This stylesheet applies some basic styles at the global level. 31 | 1. `public/css/a11y-dark.min.css`: This stylesheet applies syntax highlighting to code blocks. Feel free to swap this out with another [HighlightJS theme](https://highlightjs.org/static/demo/) of your choosing (and update the link to this stylesheet in your `head.mjs` file). 32 | 1. `app/lib/markdown-class-mappings.mjs`: This file exports an object of HTML element names matched to arrays of classes from [Enhance’s utility class system](https://enhance.dev/docs/learn/concepts/styling/utility-classes). When your markdown files are converted to HTML, these classes will be attached to the respective HTML elements. 33 | 1. ` 20 | 25 | ` 26 | } 27 | 28 | return html` 29 | 68 | 69 | 70 | 71 | 💬 Webmentions72 | 73 | ${mentions?.length ? ` 74 |
No mentions yet! 106 | `} 107 | ` 108 | } 109 | -------------------------------------------------------------------------------- /app/elements/blog-card.mjs: -------------------------------------------------------------------------------- 1 | export default function BlogPost({ html, state }) { 2 | const { attrs, store } = state 3 | const { key } = attrs 4 | const { href, frontmatter } = store.posts[key] 5 | const { description, published, readtime, title } = 6 | frontmatter 7 | return html` 8 | 18 | 19 |
21 |
28 | ${title}22 |${description} 23 |
24 | ${published}
29 | ${label}
30 | `}
31 |
43 | ${name ? `${name}` : '' }
44 | ${honorificPrefix ? `${honorificPrefix}` : '' }
45 | ${givenName ? `${givenName}` : '' }
46 | ${additionalName ? `${additionalName}` : '' }
47 | ${familyName ? `${familyName}` : '' }
48 | ${sortString ? `${sortString}` : '' }
49 | ${honorificSuffix ? `${honorificSuffix}` : '' }
50 | ${email ? `e-mail` : '' }
51 | ${logo ? `logo` : '' }
52 | ${photo ? `
84 | `
85 | }
86 |
--------------------------------------------------------------------------------
/app/elements/my-h-card.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('@enhance/types').EnhanceElemFn} */
2 | export default function MyHCard({ html, state: { store } }) {
3 | const { hCard } = store
4 | const attributes = Object.keys(hCard).map(key => `${key}="${hCard[key]}" `).join('')
5 | return html`
6 | 10 | 11 | Enhance Blog Template 12 | 13 |14 |15 | A subtitle for this blog 16 | 17 |59 | Mentions60 |
Admin7 | 8 |${title}17 |${published} 18 |
|