27 |
28 |
29 | {% include "pagination.njk" %}
30 |
--------------------------------------------------------------------------------
/tests/navigation.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect, Page } from '@playwright/test';
2 |
3 | let siteURL;
4 |
5 | test.beforeEach(async ({page, baseURL}) => {
6 | await page.goto('/');
7 | siteURL = baseURL;
8 | });
9 |
10 |
11 | test.describe('Navigation Tests', () => {
12 |
13 | test('MORE link test', async ({ page }) => {
14 | await page.click('text=More');
15 | expect(page).toHaveURL(siteURL + '2/');
16 | expect(await page.locator('text=Previous')).toBeVisible();
17 | expect(await page.locator('text=Next')).toBeVisible();
18 | await page.click('text=Previous');
19 | expect(page).toHaveURL(siteURL);
20 | });
21 |
22 | });
23 |
--------------------------------------------------------------------------------
/drafts/2023-01-07-a-draft-post.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: This draft post should only appear locally
3 | description: This should only be visible locally
4 | tags: second tag
5 |
6 | ---
7 |
8 | This post has been created in the `drafts` folder. It should only appear when the eleventy server runs with `--watch` or `--serve`.
9 |
10 | In package.json, the start command has been given those flags, so drafts should appear when running via `npm run start`.
11 |
12 | In the docker-compose file from where this site runs, the command is `npm start`, so running via `docker-compose up` should show draft posts.
13 |
14 | Since `npm run build` doesn't have the flags, drafts will not appear in the production build.
15 |
--------------------------------------------------------------------------------
/tests/post-with-github-repo-card.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect, Page } from '@playwright/test';
2 |
3 | let siteURL;
4 |
5 | test.beforeEach(async ({page, baseURL}) => {
6 | await page.goto('github-repo-card/');
7 | siteURL = baseURL;
8 | });
9 |
10 |
11 | test.describe('Post with Github Repo Card', () => {
12 |
13 | test('Repo Card is rendered with description', async ({ page }) => {
14 |
15 | await expect(page.getByText('Lightweight GPS Logging Application For Android')).toBeVisible();
16 |
17 | let sendLocator = page.getByText('mozilla/send');
18 | const mozillaSendCount = await sendLocator.count();
19 | await expect(mozillaSendCount).toBe(2);
20 |
21 | });
22 |
23 | });
24 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/tests/post-with-notice.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect, Page } from '@playwright/test';
2 |
3 | let siteURL;
4 |
5 | test.beforeEach(async ({page, baseURL}) => {
6 | await page.goto('/');
7 | siteURL = baseURL;
8 | });
9 |
10 |
11 | test.describe('Post with notice block', () => {
12 |
13 | test('Notice block is visible', async ({ page }) => {
14 | await page.goto('post-notice/');
15 | let noticeBlock = await page.locator('.notice.success');
16 | await expect(noticeBlock).toBeVisible();
17 | await expect(await noticeBlock.innerText()).toBe('This is a success notice. Great Success.');
18 | await expect(noticeBlock).toHaveCSS('background-color', 'rgb(217, 237, 217)');
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/_configs/gallery.shortcode.js:
--------------------------------------------------------------------------------
1 | /**
2 | * The `gallery` paired shortcode shows a set of images and displays it in a grid.
3 | * It sets inGallery = true, which is used by the markdown image renderer
4 | */
5 | export default function gallery(data, caption="", markdownLibrary) {
6 | // Count the number of images passed in (one per newline).
7 | // If it's an even number of items, we'll get the image to set width = 50%.
8 | // To help with the layout.
9 | let evenItems = (data.trim().split('\n').length % 2) == 0;
10 |
11 | const galleryContent = markdownLibrary.renderInline(data, { 'inGallery': true, 'evenItems': evenItems });
12 | return `${galleryContent}${markdownLibrary.renderInline(caption)}`;
13 | }
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/_data/metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Eleventy Satisfactory",
3 | "url": "https://code.mendhak.com/eleventy-satisfactory/",
4 | "pathPrefix": "/eleventy-satisfactory/",
5 | "language": "en",
6 | "paginationSize": 4,
7 | "favicon": "/assets/images/favicon.png",
8 | "description": "Blog theme for Eleventy with various new features",
9 | "tags": ["eleventy", "blog", "theme", "template", "starter"],
10 | "feed": {
11 | "path": "/feed.xml"
12 | },
13 | "jsonfeed": {
14 | "path": "/feed.json"
15 | },
16 | "author": {
17 | "name": "mendhak",
18 | "email": "",
19 | "url": "https://github.com/mendhak/eleventy-satisfactory"
20 | },
21 | "opengraph": {
22 | "image": "/assets/images/article.png",
23 | "type": "article"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/tests/post-with-github-gist.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect, Page } from '@playwright/test';
2 |
3 | let siteURL;
4 |
5 | test.beforeEach(async ({page, baseURL}) => {
6 | await page.goto('post-with-github-gists/');
7 | siteURL = baseURL;
8 | });
9 |
10 |
11 | test.describe('Post with Github Gist', () => {
12 |
13 | test('Github Gist is rendered seamlessly', async ({ page }) => {
14 |
15 | await expect(page.getByText('Hello, from Github Gist!')).toBeVisible();
16 |
17 | // await expect(page.waitForSelector('html body main article table thead tr th'))
18 | let tableSelector = page.locator('article table th').first();
19 | await expect(await tableSelector.innerText()).toBe('Option');
20 | await expect(tableSelector).toBeVisible();
21 |
22 | });
23 |
24 | });
25 |
--------------------------------------------------------------------------------
/_configs/lightboxref.shortcode.js:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import nunjucks from "nunjucks";
3 |
4 | /**
5 | * If the given post contains a figure shortcode or a markdown image, this function adds the SimpleLightbox JS and CSS so images are displayed with a lightbox.
6 | * @param {JSON} page - the eleventy page object, from which the `.inputPath` to the page will be read.
7 | * @returns
8 | */
9 | export default function getLightBoxIfNecessary(page){
10 |
11 | const str = fs.readFileSync(page.inputPath, 'utf8');
12 |
13 | if (str.includes('{% figure') || str.includes('![')) {
14 | nunjucks.configure({ trimBlocks: true, lstripBlocks: true });
15 | let lightboxHtml = nunjucks.render('_includes/simplelightbox.njk');
16 | return lightboxHtml;
17 | }
18 |
19 | return ``;
20 | }
21 |
22 |
--------------------------------------------------------------------------------
/_configs/excerpt.shortcode.js:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * Gets the first 30 words as the excerpt or until the newline, whichever comes first.
4 | */
5 | export default function (article) {
6 | if (!Object.prototype.hasOwnProperty.call(article, "templateContent")) {
7 | console.warn(
8 | 'Failed to extract excerpt: Document has no property "templateContent".'
9 | );
10 | return null;
11 | }
12 |
13 | const content = article.templateContent;
14 |
15 | //Take the first paragraph until newline, remove HTML
16 | let words = content.slice(0, content.indexOf("\n")).replace(/<[^>]*>?/gm, '').split(/\s+/);
17 | let suffix = '';
18 |
19 | if (words.length > 30) {
20 | suffix = '…';
21 | }
22 |
23 | let excerpt = words.slice(0, 30).join(' ') + suffix;
24 | return excerpt;
25 | }
26 |
27 |
28 |
--------------------------------------------------------------------------------
/tests/draft-post.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect, Page } from '@playwright/test';
2 |
3 | let siteURL;
4 |
5 | test.beforeEach(async ({page, baseURL}) => {
6 | await page.goto('/');
7 | siteURL = baseURL;
8 | });
9 |
10 |
11 |
12 | test.describe('Draft Post Tests', () => {
13 |
14 | test('Verify Draft Post is only visible locally', async ({ page }) => {
15 | await page.getByRole('link', {name: 'This draft post should only appear locally', exact: true} ).click();
16 | expect(page).toHaveURL(siteURL + 'a-draft-post/');
17 |
18 | // Ensure the draft post is not visible on the live site
19 | await page.goto('https://code.mendhak.com/eleventy-satisfactory/a-draft-post/')
20 | expect(await page.locator('text=Content not found.')).toBeVisible();
21 | });
22 |
23 | });
24 |
--------------------------------------------------------------------------------
/_data/photostream.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "link": "https://www.flickr.com/photos/mendhak/30454355997/",
4 | "img": "https://live.staticflickr.com/1932/30454355997_287063f84b_q.jpg"
5 | },
6 | {
7 | "link": "https://www.flickr.com/photos/mendhak/31366195988/",
8 | "img": "https://live.staticflickr.com/1917/31366195988_3b8862fd7e_q.jpg"
9 | },
10 | {
11 | "link": "https://www.flickr.com/photos/mendhak/38225459035/",
12 | "img": "https://live.staticflickr.com/4596/38225459035_db33403af9_q.jpg"
13 | },
14 | {
15 | "link": "https://www.flickr.com/photos/mendhak/23816072218/",
16 | "img": "https://live.staticflickr.com/4478/23816072218_c6f2a65658_q.jpg"
17 | },
18 | {
19 | "link": "https://www.flickr.com/photos/mendhak/37937678644/",
20 | "img": "https://live.staticflickr.com/4521/37937678644_e569ddc8f6_q.jpg"
21 | }
22 | ]
23 |
--------------------------------------------------------------------------------
/.github/workflows/playwright.yml:
--------------------------------------------------------------------------------
1 | name: Playwright Tests
2 | on:
3 | push:
4 | branches:
5 | - '*'
6 | pull_request:
7 |
8 |
9 | # Allows you to run this workflow manually from the Actions tab
10 | workflow_dispatch:
11 |
12 | jobs:
13 | playwright-tests:
14 | timeout-minutes: 60
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v4
18 | - uses: actions/setup-node@v4
19 | with:
20 | node-version: lts/*
21 | - name: Install dependencies
22 | run: npm ci
23 | - name: Install Playwright Browsers
24 | run: npx playwright install --with-deps
25 | - name: Run Playwright tests
26 | run: npx playwright test
27 | # - uses: actions/upload-artifact@v4
28 | # if: always()
29 | # with:
30 | # name: playwright-report
31 | # path: playwright-report/
32 | # retention-days: 30
33 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/posts/2022-01-05-set-footer-links.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Set footer images and links
3 | extraWideMedia: false
4 | ---
5 |
6 | How to set the links in the footer as well as the small thumbnails that appear.
7 |
8 | ### Links
9 |
10 | The links are controlled via the `_data/bottomlinks.json` file. It's just an array of link URLs and the text to display.
11 |
12 | ```json
13 | [
14 | {
15 | "link": "https://example.com",
16 | "text": "Example"
17 | }
18 | ...
19 | ]
20 |
21 | ```
22 |
23 | You can also link to relative paths.
24 |
25 | To remove the links in the footer, empty the array, or empty the file, or delete the file.
26 |
27 |
28 | ### Photos
29 |
30 | The photos are controlled in the `_data/photostream.json` file. It's an array of thumbnails and the URL to link to.
31 |
32 | ```json
33 | [
34 | {
35 | "link": "https://www.flickr.com/photos/mendhak/30454355997/",
36 | "img": "https://live.staticflickr.com/1932/30454355997_287063f84b_q.jpg"
37 | },
38 | ...
39 | ```
40 |
41 | To remove the images in the footer, empty the array, or empty the file, or delete the file.
42 |
--------------------------------------------------------------------------------
/posts/2022-11-11-extra-wide-full-width-images-videos.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: About extra-wide images, videos and code blocks
3 |
4 | ---
5 |
6 | By default, images, videos and code blocks are displayed wider than the (text) content around it. This is to make it stand out and give it a little emphasis. An example of this 'full bleed' behavior can be seen on the [lorem ipsum page](/posts/customary-lorem-ipsum.md).
7 |
8 |
9 | {% figure "https://live.staticflickr.com/7280/27837260711_a4f38ee02e_k.jpg", "Example of extra wide image","" %}
10 |
11 |
12 | In some cases this might be a bit overwhelming or distracting.
13 |
14 | This behavior can be disabled on a per-post basis by setting the value `extraWideMedia: false` in the frontmatter of the post. An example of this can be seen on the [footer images and links page](/posts/2022-01-05-set-footer-links.md).
15 |
16 |
17 |
18 | To disable it for all posts, set the value in the `posts/posts.json` file.
19 |
20 | ```json
21 | {
22 | ...
23 | "extraWideMedia": false
24 | }
25 |
26 | ```
27 |
28 |
29 | Note that disabling the `extraWideMedia` setting will also disable unconstrained (full width) images.
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Zach Leatherman @zachleat
4 | Copyright (c) 2023 mendhak
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 |
--------------------------------------------------------------------------------
/pages/json.njk:
--------------------------------------------------------------------------------
1 | ---
2 | # Metadata comes from _data/metadata.json
3 | permalink: "{{ metadata.jsonfeed.path }}"
4 | eleventyExcludeFromCollections: true
5 | ---
6 | {
7 | "version": "https://jsonfeed.org/version/1.1",
8 | "title": "{{ metadata.title }}",
9 | "language": "{{ metadata.language }}",
10 | "home_page_url": "{{ metadata.url }}",
11 | "feed_url": "{{ metadata.jsonfeed.path | htmlBaseUrl(metadata.url) }}",
12 | "description": "{{ metadata.description }}",
13 | "author": {
14 | "name": "{{ metadata.author.name }}",
15 | "url": "{{ metadata.author.url }}"
16 | },
17 | "items": [
18 | {%- for post in collections.posts | reverse %}
19 | {%- set absolutePostUrl %}{{ post.url | htmlBaseUrl(metadata.url) }}{% endset -%}
20 | {
21 | "id": "{{ absolutePostUrl }}",
22 | "url": "{{ absolutePostUrl }}",
23 | "title": "{{ post.data.title }}",
24 | "content_html": {% if post.templateContent %}{{ post.templateContent | htmlToAbsoluteUrls(absolutePostUrl) | dump | safe }}{% else %}""{% endif %},
25 | "date_published": "{{ post.date | dateToRfc3339 }}"
26 | }
27 | {%- if not loop.last -%}
28 | ,
29 | {%- endif -%}
30 | {%- endfor %}
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/pages/feed.njk:
--------------------------------------------------------------------------------
1 | ---
2 | # Metadata comes from _data/metadata.json
3 | permalink: "{{ metadata.feed.path }}"
4 | eleventyExcludeFromCollections: true
5 | ---
6 |
7 |
8 | {{ metadata.title }}
9 | {{ metadata.description }}
10 | {% set absoluteUrl %}{{ metadata.feed.path | htmlBaseUrl(metadata.url) }}{% endset %}
11 |
12 |
13 | {{ collections.posts | getNewestCollectionItemDate | dateToRfc3339 }}
14 | {{ metadata.url }}
15 |
16 | {{ metadata.author.name }}
17 | {{ metadata.author.email }}
18 |
19 | {%- for post in collections.posts | reverse %}
20 | {% set absolutePostUrl %}{{ post.url | htmlBaseUrl(metadata.url) }}{% endset %}
21 |
22 | {{ post.data.title }}
23 |
24 | {{ post.date | dateToRfc3339 }}
25 | {{ absolutePostUrl }}
26 | {{ post.templateContent | htmlToAbsoluteUrls(absolutePostUrl) }}
27 |
28 | {%- endfor %}
29 |
30 |
--------------------------------------------------------------------------------
/tests/post-with-image-gallery.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect, Page } from '@playwright/test';
2 |
3 | let siteURL;
4 |
5 | test.beforeEach(async ({page, baseURL}) => {
6 | await page.goto('/');
7 | siteURL = baseURL;
8 | });
9 |
10 |
11 |
12 | test.describe('Post with image gallery', () => {
13 |
14 | test('Clicking an image produces a lightbox', async ({ page }) => {
15 | await page.goto('post-with-a-gallery/');
16 | let clickableImages = await page.locator('figure a');
17 | clickableImages.nth(1).scrollIntoViewIfNeeded();
18 | clickableImages.nth(1).click();
19 | await expect(page.getByText('Second Image')).toBeVisible();
20 |
21 | await page.keyboard.press('Escape');
22 |
23 | await expect(page.getByText('Three Two images side by side')).toBeVisible();
24 |
25 | // get the image just above this
26 | let nearestImage = page.locator('figure a:near(:text("Three Two images side by side"))').last();
27 | nearestImage.scrollIntoViewIfNeeded();
28 | await expect(nearestImage).toBeVisible();
29 | await nearestImage.click();
30 | await expect(page.getByText('Second image')).toBeVisible();
31 | await page.keyboard.press('Escape');
32 | await expect(page.getByText('Second image')).not.toBeVisible();
33 |
34 | });
35 |
36 | });
37 |
--------------------------------------------------------------------------------
/tests/post-with-relative-links.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect, Page } from '@playwright/test';
2 |
3 | let siteURL;
4 |
5 | test.beforeEach(async ({page, baseURL}) => {
6 | await page.goto('/');
7 | siteURL = baseURL;
8 | });
9 |
10 |
11 | test.describe('Posts with relative links', () => {
12 |
13 | test('Test a link using a markdown path', async ({ page }) => {
14 | await page.goto('posting-links/');
15 | let link = await page.getByRole('link', { name: 'Link using Markdown path', exact: true });
16 | await link.click();
17 | expect(page.url()).toBe(siteURL + 'customary-lorem-ipsum/');
18 |
19 | await page.goBack();
20 |
21 | link = await page.getByRole('link', { name: 'Link using Markdown path and heading anchor', exact: true });
22 | await link.click();
23 | expect(page.url()).toBe(siteURL + 'customary-lorem-ipsum/#tincidunt-arcu-non-sodales');
24 |
25 | await page.goBack();
26 |
27 | link = await page.getByRole('link', { name: 'Link using root-relative URL', exact: true });
28 | await link.click();
29 | expect(page.url()).toBe(siteURL + 'customary-lorem-ipsum/');
30 |
31 | await page.goBack();
32 |
33 | link = await page.getByRole('button', { name: 'Button with a link', exact: true });
34 | await link.click();
35 | expect(page.url()).toBe(siteURL );
36 |
37 |
38 | });
39 | });
40 |
--------------------------------------------------------------------------------
/tests/post-with-images.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect, Page } from '@playwright/test';
2 |
3 | let siteURL;
4 |
5 | test.beforeEach(async ({page, baseURL}) => {
6 | await page.goto('/');
7 | siteURL = baseURL;
8 | });
9 |
10 |
11 | test.describe('Post with images', () => {
12 |
13 | test('Clicking an image produces a lightbox', async ({ page }) => {
14 | await page.goto('post-with-an-image/');
15 | let clickableImages = await page.locator('figure a');
16 | clickableImages.nth(0).scrollIntoViewIfNeeded();
17 | await clickableImages.nth(0).click();
18 | let lightbox = await page.locator('.sl-wrapper');
19 | await expect(lightbox).toBeVisible();
20 |
21 | // Ensure the URL is reset when lightbox is closed
22 | expect(await page).toHaveURL('post-with-an-image/#pid=1');
23 | await page.waitForTimeout(200);
24 | await page.keyboard.press('Escape');
25 | await page.keyboard.press('Escape');
26 | await page.waitForTimeout(300);
27 | expect(lightbox).not.toBeVisible();
28 | expect(await page).toHaveURL(siteURL + 'post-with-an-image/');
29 |
30 | clickableImages.nth(2).scrollIntoViewIfNeeded();
31 | await clickableImages.nth(2).click();
32 | await expect(lightbox).toBeVisible();
33 | await page.keyboard.press('Escape');
34 | await page.keyboard.press('Escape');
35 | await page.waitForTimeout(200);
36 | expect(await page).toHaveURL(siteURL + 'post-with-an-image/');
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/_configs/markdownlibrary.renderer.image.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Wrap images in a figure, a, and figcaption.
3 | * This lets the simplelightbox code serve it up too!
4 | * Also adds loading lazy attribute
5 | */
6 |
7 | export default function (tokens, idx, options, env, slf, markdownLibrary) {
8 |
9 | const token = tokens[idx];
10 | // Set the loading=lazy attribute
11 | token.attrSet('loading', 'lazy');
12 |
13 | let captionRendered = markdownLibrary.renderInline(token.content);
14 |
15 |
16 | if (env.inGallery) {
17 | // This is a gallery of images, so display the caption in the lightbox (by setting its title),
18 | // and only return an image, because the gallery is taking care of the .
19 | // This is because the caption might be too long and awkward to display
20 | // in a crowded area.
21 | token.attrSet('title', captionRendered);
22 | token.attrSet('style', "width: calc(33% - 0.5em);");
23 | if (env.evenItems) {
24 | token.attrSet('style', "width: calc(50% - 0.5em);");
25 | }
26 |
27 | return `${slf.renderToken(tokens, idx, options)}`;
28 | }
29 |
30 | // This is a standalone image, so return figure with figcaption.
31 | // The 'a' is the image linking to itself, which then gets picked up by simplelightbox
32 | return `
33 | ${slf.renderToken(tokens, idx, options)}
34 | ${captionRendered}
35 | `;
36 | }
37 |
--------------------------------------------------------------------------------
/posts/2022-12-20-github-repo-card.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Post with Github Repo Cards
3 | description: Widget to showcase a single repo
4 |
5 | ---
6 |
7 | How to display a Github repo card. Fetches the description, stars and forks.
8 |
9 | To display it on a page use the `githubrepocard` shortcode.
10 |
11 | ```
12 | {% raw %}{% githubrepocard 'mendhak/gpslogger' %}{% endraw %}
13 | ```
14 |
15 | Produces:
16 |
17 | {% githubrepocard 'mendhak/gpslogger' %}
18 |
19 | This should also work for third party Github repos, not just yours.
20 |
21 | ```
22 | {% raw %}{% githubrepocard 'mozilla/send' %}{% endraw %}
23 | ```
24 |
25 | Produces:
26 |
27 | {% githubrepocard 'mozilla/send' %}
28 |
29 |
30 | ### Special note about Github Repo Cards and rate limits
31 |
32 | If the Repo Card area start to appear empty sometimes, this could be because the IP address you ran the build from was rate limited by Github. This can commonly happen when running from Github Actions.
33 |
34 | If running from Github Actions, no need to do anything, the [automatically generated `GITHUB_TOKEN`](https://docs.github.com/en/actions/security-guides/automatic-token-authentication) should get passed in to the action and used by the API calls.
35 |
36 | If not running on Github Actions, you'll need to [generate a token on Github](https://github.com/settings/tokens) with the `repos` permission. Then pass that as a `--githubtoken` argument when running the `npm run build` or `npm run serve` commands, for example:
37 |
38 | ```bash
39 | npm run serve --githubtoken=xxxxxxxxxxxxxxxxx
40 | ```
41 |
--------------------------------------------------------------------------------
/tests/robots-feeds.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect, Page } from '@playwright/test';
2 |
3 | let siteURL;
4 |
5 | test.beforeEach(async ({page, baseURL}) => {
6 | await page.goto('/');
7 | siteURL = baseURL;
8 | });
9 |
10 | test.describe('Robots and Feeds and Well Known Tests', () => {
11 | test('Verify Robots.txt', async ({ page }) => {
12 | await page.goto('robots.txt');
13 | expect(page).toHaveURL(siteURL + 'robots.txt');
14 | expect(await page.textContent('*')).toContain('Allow: /');
15 | expect(await page.textContent('*')).toContain('Sitemap: ');
16 | expect(await page.textContent('*')).toMatch(/Sitemap: .+sitemap.xml/);
17 | });
18 |
19 | test('Verify Atom Feed' , async ({ page }) => {
20 | await page.goto('feed.xml');
21 | expect(page).toHaveURL(siteURL + 'feed.xml');
22 | expect(await page.textContent('*')).toContain('Eleventy Satisfactory');
23 | });
24 |
25 | test('Verify JSON feed', async ({ page }) => {
26 | await page.goto('feed.json');
27 | expect(page).toHaveURL(siteURL + 'feed.json');
28 | let feedJSON = JSON.parse(await page.innerText('*'));
29 | expect(feedJSON.title).toBe('Eleventy Satisfactory');
30 | });
31 |
32 |
33 | test('Verify .well-known URL', async ({ page }) => {
34 | await page.goto('.well-known/example.json');
35 | expect(page).toHaveURL(siteURL + '.well-known/example.json');
36 | let wellKnown = (await page.innerText('*'));
37 | let wellKnownJSON = JSON.parse(wellKnown);
38 | expect(wellKnownJSON.eleventy).toBe('satisfactory');
39 | });
40 |
41 |
42 | });
43 |
--------------------------------------------------------------------------------
/posts/2022-02-02-set-date-of-post.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: How to set the date of a post
3 | last_modified_at: 2022-08-17
4 | ---
5 |
6 | When writing a new post, you can set the date of the post in a few different ways. The date of the post determines the order it appears in the post listings.
7 |
8 | ### In the filename
9 |
10 | The date can be set in the post's filename. For example, `{{ page.inputPath }}` will be published to the URL `{{ page.url }}` and the date appears as `{{ page.date | readableDate }}`. This is the best way to write posts, since having the date at the beginning helps organize the posts in order.
11 |
12 | ### In the frontmatter
13 |
14 | The date can also be set in the page's frontmatter.
15 |
16 | ```
17 | ---
18 | title: How to set the date of a post
19 | date: 2022-02-02
20 | ---
21 | ```
22 |
23 | ### Both the file name _and_ the frontmatter
24 |
25 | If the date is set both in the file name as well as the frontmatter, the frontmatter date takes precedence.
26 |
27 |
28 | ### Setting a last modified date for a post
29 |
30 | Setting the `last_modified_at` in the frontmatter modifies the display and metadata date, but without changing its order in the listing.
31 |
32 | ```
33 | ---
34 | title: How to set the date of a post
35 | last_modified_at: 2022-08-17
36 | ---
37 | ```
38 |
39 |
40 | ### Posts without a date
41 |
42 | If there's no date in the file name, or the frontmatter, then the date defaults to the time of build, so it gets updated each time the site is built.
43 |
44 | ---
45 |
46 | Anyway, the date appears at the bottom.
47 |
48 | 👇
49 |
--------------------------------------------------------------------------------
/_configs/video.shortcode.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Paired shortcode to display an iframe video. Detects YouTube or Vimeo.
3 | */
4 |
5 | export default function(url, width) {
6 | let style = '';
7 | let iframeUrl = '';
8 | if(width==="unconstrained"){
9 | style="max-width: unset;";
10 | }
11 |
12 | let youTubeId = getVideoIdFromYouTubeURL(url);
13 | let timeStamp = getYouTubeTimeStampFromURL(url);
14 |
15 | if(youTubeId){
16 | iframeUrl = `https://www.youtube.com/embed/${youTubeId}${timeStamp}`;
17 | }
18 | else {
19 | let vimeoId = getVideoIdFromVimeoURL(url);
20 | iframeUrl = `https://player.vimeo.com/video/${vimeoId}`;
21 | }
22 |
23 | let videoHtml = `
24 |
25 |
`;
26 |
27 | return videoHtml;
28 |
29 |
30 | };
31 |
32 | function getYouTubeTimeStampFromURL(url) {
33 | let regExp = /[?&]t=(?:(\d+)m(\d+)s|(\d+)s)/;
34 |
35 | let match = url.match(regExp);
36 | return match ?
37 | (match[1] && match[2] ? `?start=${match[1]*60 + parseInt(match[2])}` :
38 | match[3] ? `?start=${match[3]}` : '') : '';
39 | }
40 |
41 | function getVideoIdFromYouTubeURL(url) {
42 | let regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
43 | let match = url.match(regExp);
44 |
45 | return match && match[2].length === 11 ? match[2] : null;
46 | }
47 |
48 | function getVideoIdFromVimeoURL(url){
49 |
50 | let regExp = /^https:\/\/vimeo\.com\/([^#&?]*)/;
51 | let match = url.match(regExp);
52 | return match && match[1] ? match[1] : null;
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "eleventy-satisfactory",
3 | "version": "1.0.0",
4 | "description": "My simplified opinionated eleventy theme, the focus is on content first, and less on other things.",
5 | "type": "module",
6 | "scripts": {
7 | "build": "npx @11ty/eleventy",
8 | "bench": "DEBUG=Eleventy:Benchmark* npx @11ty/eleventy",
9 | "start": "npx @11ty/eleventy --serve --watch --incremental",
10 | "debug": "DEBUG=* npx @11ty/eleventy",
11 | "pretest": "playwright install",
12 | "test": "playwright test --headed",
13 | "stop": "kill -9 $(lsof -t -i :8080)"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "https://github.com/mendhak/eleventy-satisfactory.git"
18 | },
19 | "author": {
20 | "name": "mendhak",
21 | "email": "mendhak@users.noreply.github.com",
22 | "url": "https://code.mendhak.com/"
23 | },
24 | "license": "MIT",
25 | "bugs": {
26 | "url": "https://github.com/mendhak/eleventy-satisfactory/issues"
27 | },
28 | "homepage": "https://code.mendhak.com/eleventy-satisfactory/",
29 | "dependencies": {
30 | "@11ty/eleventy": "^3.1.2",
31 | "@11ty/eleventy-plugin-rss": "^2.0.4",
32 | "@11ty/eleventy-plugin-syntaxhighlight": "^5.0.2",
33 | "clean-css": "^5.3.3",
34 | "dotenv": "^17.2.1",
35 | "luxon": "^3.7.1",
36 | "markdown-it": "^14.1.0",
37 | "markdown-it-anchor": "^9.0.1",
38 | "prism-themes": "^1.9.0",
39 | "simpledotcss": "2.1.1",
40 | "simplelightbox": "^2.14.3"
41 | },
42 | "engines": {
43 | "node": ">=18.0.0"
44 | },
45 | "devDependencies": {
46 | "@playwright/test": "^1.54.2",
47 | "@types/node": "^24.1.0"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/posts/2022-03-03-opengraph-preview-data.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: How to set Opengraph preview image and metadata
3 | description: Instructions on setting up various Opengraph headers
4 | tags:
5 | - opengraph
6 | - image
7 | - preview
8 | opengraph:
9 | image: /assets/images/image003.jpg
10 | ---
11 |
12 | When sharing a URL, some sites will generate a preview with a title, description and image using certain [OpenGraph](https://ogp.me/) metadata.
13 |
14 | ### Set the preview image
15 |
16 | A default Opengraph preview image for all posts is set in the [metadata.json](/posts/2022-01-01-edit-the-metadata.md#opengraph), which looks like:
17 |
18 | {% figure metadata.opengraph.image, "Default opengraph image", "third" %}
19 |
20 |
21 | To override the preview image for a specific post, set the `opengraph.image` value in the frontmatter:
22 |
23 | ```
24 | ---
25 | title: How to set Opengraph preview image and metadata
26 | opengraph:
27 | image: /assets/images/image003.jpg
28 | ---
29 | ```
30 |
31 | ### Title, description, and tags
32 |
33 | All three of these are taken from the post's frontmatter's title, description, and tags.
34 |
35 | ```
36 | ---
37 | title: How to set Opengraph preview image and metadata
38 | description: Instructions on setting up various Opengraph headers
39 | tags:
40 | - opengraph
41 | - image
42 | - preview
43 | opengraph:
44 | image: /assets/images/image003.jpg
45 | ---
46 | ```
47 |
48 | If missing, the defaults are taken from the [metadata.json](/posts/2022-01-01-edit-the-metadata.md#optional-but-useful) again.
49 |
50 |
51 | ### Author and type
52 |
53 | The author and type are always taken from the metadata.json author and type.
54 |
--------------------------------------------------------------------------------
/_configs/figure.shortcode.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Paired shortcode to display a figure with caption.
3 | * This is very similar to the regular Markdown image,
4 | * But I'll keep this around in case the other way ever breaks in the future
5 | * Plus, this has the 'width' flexibility, and maybe more future features.
6 | */
7 | export default function (image, caption, widthName, useLightbox, markdownLibrary) {
8 |
9 | let width = '';
10 | switch (widthName) {
11 | case 'half':
12 | width = 'width: calc(50% - 0.5em);';
13 | break;
14 | case 'third':
15 | width = 'width: calc(33% - 0.5em);';
16 | break;
17 | case 'unconstrained':
18 | width = 'max-width: unset';
19 | break;
20 | default:
21 | break;
22 | }
23 |
24 | let captionMarkup = "";
25 | let linkOpen = "", linkClose = "";
26 | if (caption !== undefined && caption !== "") {
27 | captionMarkup = markdownLibrary.renderInline(caption);
28 | }
29 |
30 | if(useLightbox){
31 | // We've configure simplelightbox to render if there's a `figure > a`.
32 | linkOpen = ``;
33 | linkClose = ``;
34 | }
35 |
36 | let rendered = `${linkOpen}${linkClose}${captionMarkup}`;
37 | if(widthName==='unconstrained'){
38 | //Since it's the image's 100% size anyway, there's no point in giving it a lightbox. Just wrap it in a figure tag, so it gets centered at least.
39 | rendered = `${captionMarkup}`;
40 | }
41 |
42 | return rendered;
43 | }
44 |
45 |
46 |
--------------------------------------------------------------------------------
/assets/css/github.repocard.css:
--------------------------------------------------------------------------------
1 | .github-repo-card {
2 | --gh-bg-color: #fff;
3 | --gh-color: #586069;
4 | --gh-heading-color: #0366d6;
5 | font-family: var(--sans-font);
6 | width: fit-content;
7 | max-width: 50%;
8 | background-color: var(--gh-bg-color) !important;
9 | border: 1px solid var(--gh-color) !important;
10 | border-radius: 6px !important;
11 | padding: 16px !important;
12 | color: var(--gh-color) !important;
13 | }
14 |
15 | @media screen and (max-width: 1200px) {
16 | .github-repo-card {
17 | max-width: 80%;
18 | }
19 | }
20 |
21 | @media screen and (max-width: 800px) {
22 | .github-repo-card {
23 | max-width: 100%;
24 | }
25 | }
26 |
27 | @media (prefers-color-scheme: dark) {
28 | .github-repo-card {
29 | --gh-bg-color: #212224;
30 | --gh-color: #8b949e;
31 | --gh-heading-color: #58a6ff;
32 | }
33 | }
34 |
35 | .github-repo-card svg {
36 | fill: var(--gh-color);
37 | }
38 |
39 | .github-repo-card .d-flex {
40 | display: flex !important;
41 | margin-bottom: 4px !important;
42 | align-items: flex-start !important;
43 | justify-content: space-between !important;
44 | }
45 |
46 | .github-repo-card a {
47 | color: var(--gh-heading-color) !important;
48 | }
49 |
50 | .github-repo-card .stats-icons a {
51 | display: inline-block !important;
52 | margin-right: 24px !important;
53 | color: var(--gh-color) !important;
54 | font-size: 0.95rem !important;
55 | }
56 |
57 | .github-repo-card .github-repo-text {
58 | color: var(--gh-color) !important;
59 | font-size: 1rem;
60 | display: flex !important;
61 | white-space: normal !important;
62 | margin-bottom: 8px !important;
63 | }
64 |
65 | .github-repo-card .github-repo-title {
66 | font-weight: bolder;
67 | }
68 |
--------------------------------------------------------------------------------
/tests/home-page.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect, Page } from '@playwright/test';
2 |
3 | let siteURL;
4 |
5 | test.beforeEach(async ({page, baseURL}) => {
6 | await page.goto('/');
7 | siteURL = baseURL;
8 | });
9 |
10 |
11 | test.describe('Home Page Tests', () => {
12 |
13 | test('Verify Location and Title', async ({ page}) => {
14 | await page.waitForSelector('header a');
15 | const header = await page.locator('header a').innerText();
16 | expect(page).toHaveURL(siteURL);
17 | expect(header).toBe('Eleventy Satisfactory');
18 | });
19 |
20 | test('Verify Footer Links', async ({ page }) => {
21 | await page.waitForSelector('footer');
22 |
23 | const githubLink = await page.locator('footer').first().locator("a").nth(0).getAttribute('href');
24 | expect(githubLink).toBe('https://github.com/mendhak/eleventy-satisfactory');
25 |
26 | const eleventyLink = await page.locator('footer').first().locator("a").nth(1).getAttribute('href');
27 | expect(eleventyLink).toBe('https://11ty.dev');
28 |
29 | const simpleCssLink = await page.locator('footer').first().locator("a").nth(2).getAttribute('href');
30 | expect(simpleCssLink).toBe('https://simplecss.org/');
31 |
32 | const rssFeedLink = await page.getByText('FEED').nth(0);
33 | await rssFeedLink.click();
34 | expect(page).toHaveURL(siteURL + 'feed.xml');
35 | });
36 |
37 | test('Verify Flickr Photos', async ({ page }) => {
38 | await page.waitForSelector('ul.photostream a img');
39 | const photos = await page.locator('ul.photostream').locator('a img');
40 | expect(await photos.count()).toBe(5);
41 | expect(await photos.nth(0)).toBeVisible();
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/posts/post-with-a-gallery.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Post with image galleries
3 | description: This post contains an image gallery in Markdown as well as shortcodes.
4 | tags:
5 | - eleventy
6 | - markdown
7 | - shortcode
8 | - images
9 | - gallery
10 | - lightbox
11 |
12 | opengraph:
13 | image: /assets/images/image001.jpg
14 |
15 | ---
16 |
17 | To display a set of images in a gallery, just surround the images, written as Markdown, with the `gallery` shortcode.
18 |
19 | Clicking the image shows the image in lightbox, with the title as the caption (a little Markdown might in the caption might also work).
20 |
21 | ```
22 | {% raw %}{% gallery %}
23 | 
24 | 
25 | 
26 | {% endgallery %}{% endraw %}
27 | ```
28 |
29 | Produces this:
30 |
31 | {% gallery %}
32 | 
33 | 
34 | 
35 | {% endgallery %}
36 |
37 |
38 | The image's own titles appear as captions in the lightbox, but not on the page, as it could get too crowded.
39 |
40 | ### Gallery with caption
41 |
42 | To set a caption for the gallery as a whole, pass it to the shortcode.
43 |
44 | ```
45 | {% raw %}{% gallery "~~Three~~ Two images side by side" %}
46 | 
47 | 
48 | {% endgallery %}{% endraw %}
49 | ```
50 |
51 | Produces:
52 |
53 | {% gallery "~~Three~~ Two images side by side" %}
54 | 
55 | 
56 | {% endgallery %}
57 |
58 | The gallery's caption appears on the page. The image's own captions still appear in the lightbox, just not on the page.
59 |
--------------------------------------------------------------------------------
/.github/workflows/staticsite.yml:
--------------------------------------------------------------------------------
1 |
2 | name: Build and Publish
3 |
4 | # Trigger this workflow only when the main branches, assets folder is modified.
5 | on:
6 | push:
7 | branches: [ "main", "master" ]
8 | paths-ignore:
9 | - '**/README.md'
10 | - '.github/workflows/playwright.yml'
11 | - 'tests/**'
12 |
13 |
14 | # Allows you to run this workflow manually from the Actions tab
15 | workflow_dispatch:
16 |
17 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
18 | permissions:
19 | contents: read
20 | pages: write
21 | id-token: write
22 | deployments: write
23 |
24 | jobs:
25 | build:
26 | runs-on: ubuntu-latest
27 |
28 | environment:
29 | name: github-pages
30 | url: ${{ steps.deployment.outputs.page_url }}
31 |
32 | steps:
33 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
34 | - uses: actions/checkout@v3
35 |
36 | - name: Use Node.js 20
37 | uses: actions/setup-node@v3
38 | with:
39 | node-version: 20
40 |
41 | - name: Build the static site
42 | # If you use gists, generate a GH_GIST_TOKEN and add to repository action secrets.
43 | # If GH_GIST_TOKEN doesn't exist, it's an unauthenticated call to Gist API...
44 | # Unauthenticated call gets made to Gist API from Github Actions will result in rate limits and gists will fail to load.
45 | # You can't use secrets.GITHUB_TOKEN either, as it's scoped to the repository and not the gists!
46 | run: /bin/bash -c 'npm install;npm run build --gisttoken=${{ secrets.GH_GIST_TOKEN }} --githubtoken=${{ secrets.GITHUB_TOKEN }}'
47 |
48 | - name: Setup Github Pages
49 | uses: actions/configure-pages@v2
50 |
51 | - name: Upload Github Pages artifact
52 | uses: actions/upload-pages-artifact@v3
53 |
54 | - name: Deploy to GitHub Pages
55 | id: deployment
56 | uses: actions/deploy-pages@v4
57 |
--------------------------------------------------------------------------------
/posts/2023-01-15-post-notice.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Post with notices or alert boxes
3 | description: I want to display what notice boxes would look like.
4 | tags:
5 | - notice
6 |
7 | ---
8 |
9 | This post will show how to create different kinds of notice or alert boxes such as a regular box, as well as info, success, warning, and danger.
10 |
11 | In all cases, use the paired shortcode `notice` with the notice type (info, success, warning, danger). Leave the notice type out for the regular notice box. You can use markdown inside.
12 |
13 | {% notice %}
14 | This is the default notice. You _can_ use Markdown.
15 | {% endnotice %}
16 |
17 |
18 |
19 | {% notice "info" %}
20 | This is an info notice. Info means [information](https://en.wikipedia.org/wiki/Information)
21 | {% endnotice %}
22 |
23 |
24 | {% notice "success" %}
25 | This is a success notice. Great Success.
26 | {% endnotice %}
27 |
28 |
29 | {% notice "warning" %}
30 | This is a warning notice. Don't use `GOTO` statements!
31 | {% endnotice %}
32 |
33 | {% notice "danger" %}
34 | This is a danger notice. **Smoking is fatal**.
35 | {% endnotice %}
36 |
37 |
38 | For a regular notice box,
39 |
40 | ```
41 | {% raw %}{% notice %}
42 | This is the default notice. You _can_ use Markdown.
43 | {% endnotice %}{% endraw %}
44 | ```
45 |
46 | For an info notice box,
47 |
48 | ```
49 | {% raw %}{% notice "info" %}
50 | This is an info notice. Info means [information](https://en.wikipedia.org/wiki/Information)
51 | {% endnotice %}{% endraw %}
52 | ```
53 |
54 | For a success notice box,
55 |
56 | ```
57 | {% raw %}{% notice "success" %}
58 | This is a success notice. Great Success.
59 | {% endnotice %}{% endraw %}
60 | ```
61 |
62 | For a warning notice box,
63 |
64 | ```
65 | {% raw %}{% notice "warning" %}
66 | This is a warning notice. Don't use `GOTO` statements!
67 | {% endnotice %}{% endraw %}
68 | ```
69 |
70 | For a danger notice box,
71 |
72 | ```
73 | {% raw %}{% notice "danger" %}
74 | This is a danger notice. **Smoking is fatal**.
75 | {% endnotice %}{% endraw %}
76 | ```
77 |
--------------------------------------------------------------------------------
/_configs/gist.shortcode.js:
--------------------------------------------------------------------------------
1 | import MarkdownIt from "markdown-it";
2 |
3 | /**
4 | * For a given Github Gist, render the gist's files as code blocks
5 | * @param {String} gistId - the ID of the Github Gist
6 | * @param {MarkdownIt} markdownLibrary - the instance of the markdown-it object to use to render the contents
7 | * @returns
8 | */
9 | export default async function getGist(gistId, markdownLibrary) {
10 | let url = `https://api.github.com/gists/${gistId}`;
11 | let fetchOptions = {};
12 |
13 | let gistJson = {};
14 | let mdCode = '';
15 |
16 | // If we're running in a Github Action, the token can be used to make Github API calls with better rate limits.
17 | // Otherwise, without this, sometimes the API call fails due to a low rate limit.
18 | // To pass the token, npm run build --gisttoken=${{ secrets.GH_GIST_TOKEN }}
19 | if (process.env.npm_config_gisttoken || process.env.GH_GIST_TOKEN) {
20 | let token = process.env.npm_config_gisttoken || process.env.GH_GIST_TOKEN;
21 | fetchOptions.headers = { 'Authorization': `Bearer ${token}` }
22 | }
23 |
24 | /* fetch() returns a promise, but await can be used inside addNunjucksAsyncShortcode */
25 | let response = await fetch(url, fetchOptions);
26 |
27 | if (response.ok) {
28 | gistJson = await response.json();
29 | }
30 | else {
31 | console.log(await response.json());
32 | return '';
33 | }
34 |
35 |
36 | let description = gistJson.description;
37 |
38 | for (var f in gistJson.files) {
39 | let language = gistJson.files[f].language;
40 | let content = gistJson.files[f].content
41 | let fileName = gistJson.files[f].filename;
42 |
43 | if (!language) {
44 | language = "text"
45 | }
46 | if (language === "C#") {
47 | language = "csharp";
48 | }
49 |
50 | language = language.toLowerCase();
51 |
52 |
53 | if (language === "markdown") {
54 | //Special case for MD, just render it outright.
55 | mdCode += `\n${content}\n`;
56 | }
57 | else {
58 | //Surround by code tags so it gets rendered with prism
59 | mdCode += `\`${fileName}\`\n\`\`\`${language}\n${content}\n\`\`\`\n`;
60 | }
61 |
62 | }
63 |
64 | return `**${description}**\n${markdownLibrary.render(mdCode)}`;
65 | }
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/posts/2022-12-19-post-with-github-gists.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Post with seamless Github Gists
3 | description: Using shortcode to put Github Gists on the page
4 | tags:
5 | - eleventy
6 | - github
7 | - gist
8 | - seamless
9 |
10 | ---
11 |
12 | Although it's possible to embed a gist using the shortcode that Github provides, it looks awful on dark mode. This post makes use of the `gist` shortcode, which will render each file as a regular code block so that the Gist's contents look like any other blog content.
13 |
14 |
15 | ### Github Gists through shortcode
16 |
17 | Just use the gist shortcode and give it the Gist ID which is easily visible [in the URL](https://gist.github.com/mendhak/37b74037637b5752741160058e243094).
18 |
19 | ```
20 | {% raw %}{% gist "37b74037637b5752741160058e243094" %}{% endraw %}
21 | ```
22 |
23 | The Gist description appears first, followed by each file name and the contents of the file. Here is the output:
24 |
25 |
26 | {% gist "37b74037637b5752741160058e243094" %}
27 |
28 |
29 | ### Github Gists with Markdown
30 |
31 | If a Github Gist file [contains Markdown like this one](https://gist.github.com/mendhak/770907f98223b22b422be8b5e09803ab), it'll be rendered directly onto the page.
32 |
33 | ---
34 |
35 | {% gist "770907f98223b22b422be8b5e09803ab" %}
36 |
37 |
38 |
39 | ### Special note about Github Gists and rate limits
40 |
41 | If the Gists start to appear empty sometimes, this could be because the IP address you ran the build from was rate limited by Github. This can commonly happen when running from Github Actions.
42 |
43 | To get gists to more reliably appear, [generate a token on Github](https://github.com/settings/tokens) with the `gists` permission.
44 |
45 | Under `Security > Secrets and variables > Actions` create a new secret named `GH_GIST_TOKEN` and paste that value in there.
46 |
47 | This secret value should get picked up the next time the action runs.
48 |
49 | If not running on Github Actions, you'll need to [generate a token on Github](https://github.com/settings/tokens) with the `gists` permission. Then pass that as a `--gisttoken` argument when running the `npm run build` or `npm run serve` commands, for example:
50 |
51 | ```bash
52 | npm run serve --gisttoken=xxxxxxxxxxxxxxxxx
53 | ```
54 |
55 | Or set it as an environment variable:
56 |
57 | ```bash
58 | export GH_GIST_TOKEN=xxxxxxxxxxxxxxxxx
59 | npm run serve
60 | ```
61 |
62 |
--------------------------------------------------------------------------------
/posts/2023-01-05-post-with-code.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Post with some code blocks
3 | description: This post contains sample code blocks
4 | date: 2023-01-05
5 | tags:
6 | - eleventy
7 | - markdown
8 | - code
9 |
10 | ---
11 |
12 | A post with some code samples. These are using the Prism CSS library.
13 |
14 |
15 | ### Basic code blocks using indents
16 |
17 | Any text that is indented by 4 spaces will be treated as a preformatted block.
18 |
19 | console.log("hello");
20 |
21 | ### Basic code blocks using backticks
22 |
23 | Another way to do the same thing is surround the code with 3 backticks.
24 |
25 |
26 | ` ``` `
27 | `console.log('hello'); `
28 | ` ``` `
29 |
30 | Produces:
31 |
32 | ```
33 | console.log('hello');
34 | ```
35 |
36 | ### Code blocks with syntax highlighting
37 |
38 | To get colorful syntax highlighting, follow the 3 backtickes with a [language name](https://prismjs.com/#languages-list).
39 |
40 | For example ` ```javascript`, then add the code, and close the block with ` ``` `
41 |
42 | Here is some Javascript
43 |
44 | ```javascript
45 | //If there's a JWT header, parse it and decode and put it in the response
46 | if (process.env.JWT_HEADER) {
47 | let token = req.headers[process.env.JWT_HEADER.toLowerCase()];
48 | if (!token) {
49 | echo.jwt = token;
50 | } else {
51 | token = token.split(" ").pop();
52 | const decoded = jwt.decode(token, {complete: true});
53 | echo.jwt = decoded;
54 | }
55 | }
56 |
57 | ```
58 |
59 | Similarly for C#, use ` ```csharp`
60 |
61 | ```csharp
62 | static async Task FindBucketLocationAsync(IAmazonS3 client)
63 | {
64 | string bucketLocation;
65 | var request = new GetBucketLocationRequest()
66 | {
67 | BucketName = bucketName
68 | };
69 | GetBucketLocationResponse response = await client.GetBucketLocationAsync(request);
70 | bucketLocation = response.Location.ToString();
71 | return bucketLocation;
72 | }
73 | ```
74 |
75 |
76 | Have some ` ```bash`
77 |
78 | ```bash
79 | #!/usr/bin/env bash
80 |
81 | set -euo pipefail
82 |
83 | function message {
84 | echo ""
85 | echo "---------------------------------------------------------------"
86 | echo $1
87 | echo "---------------------------------------------------------------"
88 | }
89 |
90 | if ! [ -x "$(command -v jq)" ]; then
91 | message "JQ not installed. Installing..."
92 | sudo apt -y install jq
93 | fi
94 | ```
95 |
96 |
97 |
--------------------------------------------------------------------------------
/playwright.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, devices } from '@playwright/test';
2 |
3 | /**
4 | * Read environment variables from file.
5 | * https://github.com/motdotla/dotenv
6 | */
7 | // require('dotenv').config();
8 |
9 | /**
10 | * See https://playwright.dev/docs/test-configuration.
11 | */
12 | export default defineConfig({
13 | testDir: './tests',
14 | /* Run tests in files in parallel */
15 | fullyParallel: true,
16 | /* Fail the build on CI if you accidentally left test.only in the source code. */
17 | forbidOnly: !!process.env.CI,
18 | /* Retry on CI only */
19 | retries: process.env.CI ? 2 : 0,
20 | /* Opt out of parallel tests on CI. */
21 | workers: process.env.CI ? 1 : undefined,
22 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */
23 | reporter: process.env.CI ? 'github' : 'html',
24 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
25 | use: {
26 | /* Base URL to use in actions like `await page.goto('/')`. */
27 | // baseURL: 'http://127.0.0.1:3000',
28 |
29 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
30 | trace: 'on-first-retry',
31 | screenshot: 'only-on-failure',
32 | baseURL: 'http://localhost:8080/eleventy-satisfactory/',
33 | },
34 |
35 | /* Configure projects for major browsers */
36 | projects: [
37 | // {
38 | // name: 'chromium',
39 | // use: { ...devices['Desktop Chrome'] },
40 | // },
41 |
42 | {
43 | name: 'firefox',
44 | use: { ...devices['Desktop Firefox'] },
45 | },
46 |
47 |
48 | /* Test against mobile viewports. */
49 | // {
50 | // name: 'Mobile Chrome',
51 | // use: { ...devices['Pixel 5'] },
52 | // },
53 | // {
54 | // name: 'Mobile Safari',
55 | // use: { ...devices['iPhone 12'] },
56 | // },
57 |
58 | /* Test against branded browsers. */
59 | // {
60 | // name: 'Microsoft Edge',
61 | // use: { ...devices['Desktop Edge'], channel: 'msedge' },
62 | // },
63 | // {
64 | // name: 'Google Chrome',
65 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' },
66 | // },
67 | ],
68 |
69 | /* Run your local dev server before starting the tests */
70 | webServer: {
71 | command: 'npm run start',
72 | url: 'http://127.0.0.1:8080',
73 | reuseExistingServer: !process.env.CI,
74 | },
75 | });
76 |
--------------------------------------------------------------------------------
/posts/2022-11-22-post-with-iframes-videos-third-party.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Post with some videos, audio, and other media
3 | description: This post contains sample videos using iframe
4 | tags:
5 | - eleventy
6 | - iframe
7 | - videos
8 | - third-party
9 | extraWideMedia: true
10 |
11 | ---
12 |
13 | It's common to place third party audio, video, gists, etc. on a page using iframes. There's a shortcode to do this for YouTube and Vimeo.
14 |
15 |
16 | ## Video shortcode
17 |
18 | The `video` shortcode can work with YouTube and Vimeo URLs.
19 |
20 | ### Youtube Video
21 |
22 | This code:
23 |
24 | ```
25 | {% raw %}{% video "https://www.youtube.com/embed/9qOvG9KeJ6c" %}{% endraw %}
26 | ```
27 |
28 | Produces:
29 |
30 | {% video "https://www.youtube.com/embed/9qOvG9KeJ6c" %}
31 |
32 |
33 |
34 | ### Vimeo Video
35 |
36 | The same shortcode can be used for Vimeo videos.
37 |
38 | ```
39 | {% raw %}{% video "https://vimeo.com/35396305" %}{% endraw %}
40 | ```
41 |
42 | Produces:
43 |
44 | {% video "https://vimeo.com/35396305" %}
45 |
46 |
47 | ### Unconstrained full width video
48 |
49 | Add the argument `unconstrained` to the shortcode to let the video go full width across the page.
50 |
51 | ```
52 | {% raw %}{% video "https://www.youtube.com/watch?v=DppVAQqaNE4", "unconstrained" %}{% endraw %}
53 | ```
54 |
55 | Produces:
56 |
57 | {% video "https://www.youtube.com/watch?v=DppVAQqaNE4", "unconstrained" %}
58 |
59 |
60 | ## Straight up HTML
61 |
62 | It is possible to paste the iframe code directly in Markdown. This works with YouTube, Vimeo, Twitch and any other video sources that provide HTML embed code.
63 |
64 | Remember to remove the height and width, and wrap the iframe code in a div with class="video" as shown here.
65 |
66 | ```html
67 |
68 |
69 |
70 | ```
71 |
72 | Produces:
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | ## Soundcloud audio
82 |
83 | For Soundcloud, there's no need to change the height and width. Just copy pasting the iframe code works.
84 |
85 |
42 |
--------------------------------------------------------------------------------
/posts/posting-links.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: How to post links to another post or an external site
3 | description: Various ways to post links to markdown files, relative URLs or absolute URLs
4 | date: 2022-03-01
5 | tags:
6 | - second tag
7 | - posts with two tags
8 |
9 | ---
10 |
11 | Various ways to post links to other posts in the same blog, and external links too.
12 |
13 | ### Link to another post by its file name
14 |
15 | This is a local editor friendly way, just link straight to the Markdown file.
16 | The link must start at the project root, and end in `.md`.
17 |
18 | ```markdown
19 | [Link using Markdown path](/posts/customary-lorem-ipsum.md)
20 | ```
21 |
22 | Produces:
23 |
24 | [Link using Markdown path](/posts/customary-lorem-ipsum.md)
25 |
26 | You can also link to an anchor in the target file.
27 |
28 | ```markdown
29 | [Link using Markdown path and heading anchor](/posts/customary-lorem-ipsum.md#tincidunt-arcu-non-sodales)
30 | ```
31 |
32 | Produces:
33 |
34 | [Link using Markdown path and heading anchor](/posts/customary-lorem-ipsum.md#tincidunt-arcu-non-sodales)
35 |
36 | #### Links inside `notice` and `figure`
37 |
38 | ```markdown
39 | {% raw %}{% notice "warning" %}{% endraw %}
40 | You can also link inside shortcodes such as `notice` and `figure`, [pointing at the file](/posts/customary-lorem-ipsum.md).
41 | You can also link to a heading anchor in the file [like this](/posts/customary-lorem-ipsum.md#tincidunt-arcu-non-sodales).
42 | {% raw %}{% endnotice %}{% endraw %}
43 | ```
44 | Produces:
45 |
46 | {% notice "warning" %}
47 | You can also link inside shortcodes such as `notice` and `figure`, [pointing at the file](/posts/customary-lorem-ipsum.md).
48 | You can also link to a heading anchor in the file [like this](/posts/customary-lorem-ipsum.md#tincidunt-arcu-non-sodales).
49 | {% endnotice %}
50 |
51 | ### Link to another post by URL
52 |
53 | You can also link to the output URL of the post. The blog's `pathPrefix` gets added on automatically. Remember to include the trailing slash.
54 |
55 | ```markdown
56 | [Link using root-relative URL](/customary-lorem-ipsum/)
57 | ```
58 |
59 | Produces:
60 |
61 | [Link using root-relative URL](/customary-lorem-ipsum/)
62 |
63 |
64 | ### External links
65 |
66 |
67 | ```markdown
68 | [External link](https://example.com)
69 | ```
70 |
71 | Produces:
72 |
73 | [External link](https://example.com)
74 |
75 | ### Link with a title
76 |
77 | ```markdown
78 | [Link with a title](https://example.com "title text!")
79 | ```
80 | Produces:
81 |
82 | [Link with a title](https://example.com "title text!")
83 |
84 |
85 | ## Buttons
86 |
87 | It's also possible to use a button as a link.
88 |
89 | ```
90 | {% raw %}{% button "Button with a link", "/" %}{% endraw %}
91 | ```
92 |
93 | Produces:
94 |
95 | {% button "Button with a link", "/" %}
96 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/posts/markdown-showcase.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Showcase of various `Markdown` features
3 | description: Trying out various Markdown features on a post
4 | date: 2018-07-04
5 | tags:
6 | - number 2
7 |
8 | ---
9 |
10 | This page demos various markdown syntax and how it's rendered.
11 |
12 |
13 | # h1 Heading
14 | ## h2 Heading
15 | ### h3 Heading
16 | #### h4 Heading
17 | ##### h5 Heading
18 | ###### h6 Heading
19 |
20 |
21 | ## Horizontal Rules
22 |
23 | ___
24 |
25 | ---
26 |
27 | ***
28 |
29 |
30 | ## Typographic replacements
31 |
32 | Enable typographer option to see result.
33 |
34 | (c) (C) (r) (R) (tm) (TM) (p) (P) +-
35 |
36 | test.. test... test..... test?..... test!....
37 |
38 | !!!!!! ???? ,, -- ---
39 |
40 | "Smartypants, double quotes" and 'single quotes'
41 |
42 |
43 | ## Emphasis
44 |
45 | **This is bold text**
46 |
47 | __This is bold text__
48 |
49 | *This is italic text*
50 |
51 | _This is italic text_
52 |
53 | ~~Strikethrough~~
54 |
55 |
56 | ## Blockquotes
57 |
58 |
59 | > Blockquotes can also be nested...
60 | >> ...by using additional greater-than signs right next to each other...
61 | > > > ...or with spaces between arrows.
62 |
63 |
64 | ## Lists
65 |
66 | Unordered
67 |
68 | + Create a list by starting a line with `+`, `-`, or `*`
69 | + Sub-lists are made by indenting 2 spaces:
70 | - Marker character change forces new list start:
71 | * Ac tristique libero volutpat at
72 | + Facilisis in pretium nisl aliquet
73 | - Nulla volutpat aliquam velit
74 | + Very easy!
75 |
76 | Ordered
77 |
78 | 1. Lorem ipsum dolor sit amet
79 | 2. Consectetur adipiscing elit
80 | 3. Integer molestie lorem at massa
81 |
82 |
83 | 1. You can use sequential numbers...
84 | 1. ...or keep all the numbers as `1.`
85 |
86 | Start numbering with offset:
87 |
88 | 57. foo
89 | 1. bar
90 |
91 |
92 | ## Code
93 |
94 | Inline `code`
95 |
96 | Indented code
97 |
98 | // Some comments
99 | line 1 of code
100 | line 2 of code
101 | line 3 of code
102 |
103 |
104 | Block code "fences"
105 |
106 | ```
107 | Sample text here...
108 | ```
109 |
110 | Syntax highlighting
111 |
112 | ``` js
113 | var foo = function (bar) {
114 | return bar++;
115 | };
116 |
117 | console.log(foo(5));
118 | ```
119 |
120 | ## Tables
121 |
122 | | Option | Description |
123 | | ------ | ----------- |
124 | | data | path to data files to supply the data that will be passed into templates. |
125 | | engine | engine to be used for processing templates. Handlebars is the default. |
126 | | ext | extension to be used for dest files. |
127 |
128 | Right aligned columns
129 |
130 | | Option | Description |
131 | | ------:| -----------:|
132 | | data | path to data files to supply the data that will be passed into templates. |
133 | | engine | engine to be used for processing templates. Handlebars is the default. |
134 | | ext | extension to be used for dest files. |
135 |
136 |
137 | ## Links
138 |
139 | [link text](https://example.com)
140 |
141 | [link with title](https://example.com "title text!")
142 |
143 | [link to another post](/customary-lorem-ipsum/)
144 |
145 |
146 | ## Images
147 |
148 | 
149 | 
150 |
151 | Like links, Images also have a footnote style syntax
152 |
153 | ![Alt text][id]
154 |
155 | With a reference later in the document defining the URL location:
156 |
157 | [id]: https://octodex.github.com/images/dojocat.jpg "The Dojocat"
158 |
159 |
160 |
--------------------------------------------------------------------------------
/_configs/githubrepocard.shortcode.js:
--------------------------------------------------------------------------------
1 | import CleanCSS from "clean-css";
2 | import nunjucks from "nunjucks";
3 | /**
4 | * Generates a Github Repo Card for a repository given a repo slug.
5 | * It calls the Github API to get the information out.
6 | * If this returns empty strings, you might be rate limited. See comment below.
7 | * @returns
8 | */
9 | export default async function(repoSlug){
10 |
11 | let css = getCss();
12 |
13 | let repo = await getGithubApiRepoResponse(repoSlug);
14 |
15 | if(!repo){
16 | return '';
17 | }
18 |
19 | let html = getHtml(repo);
20 |
21 | return css + html;
22 | }
23 |
24 | class GithubApiRepoResponse{
25 | repoSlug;
26 | repoLink;
27 | description;
28 | stargazers;
29 | stargazersLink;
30 | forks;
31 | networkLink;
32 | language;
33 | }
34 |
35 | /**
36 | *
37 | * @param {String} repoSlug - the username/reponame format slug of the Github repo
38 | * @returns {GithubApiRepoResponse}
39 | */
40 | async function getGithubApiRepoResponse(repoSlug){
41 | let url = `https://api.github.com/repos/${repoSlug}`;
42 | let fetchOptions = {};
43 |
44 | // If we're running in a Github Action, the token can be used to make Github API calls with better rate limits.
45 | // Otherwise, without this, sometimes the API call fails due to a low rate limit.
46 | // To pass the token, npm run build --githubtoken=${{ secrets.GITHUB_TOKEN }}
47 | if (process.env.npm_config_githubtoken || process.env.GITHUB_TOKEN) {
48 | let token = process.env.npm_config_githubtoken || process.env.GITHUB_TOKEN;
49 | fetchOptions.headers = { 'Authorization': `Bearer ${token}` }
50 | }
51 |
52 | let response = await fetch(url, fetchOptions);
53 | let githubJson = {};
54 |
55 | if(!response.ok){
56 | console.log(await response.json());
57 | return null;
58 | }
59 |
60 | githubJson = await response.json();
61 |
62 | let repo = new GithubApiRepoResponse();
63 | repo.repoSlug = repoSlug;
64 | repo.repoLink = `https://github.com/${repoSlug}`;
65 | repo.stargazers = githubJson.stargazers_count;
66 | repo.stargazersLink = `${repo.repoLink}/stargazers`;
67 | repo.forks = githubJson.forks_count;
68 | repo.networkLink = `${repo.repoLink}/network/members`;
69 | repo.description = githubJson.description;
70 | repo.language = githubJson.language;
71 |
72 | return repo;
73 | }
74 |
75 | /**
76 | *
77 | * @param {GithubApiRepoResponse} repo
78 | * @returns {String} - Single line HTML containing the markup for a Github repo card
79 | */
80 | function getHtml(repo){
81 | nunjucks.configure({ trimBlocks: true, lstripBlocks: true });
82 | let htmlBox = nunjucks.render('_includes/github.repocard.njk',
83 | {
84 | repoSlug: repo.repoSlug,
85 | repoLink: repo.repoLink,
86 | description: repo.description,
87 | stargazers: repo.stargazers,
88 | stargazersLink: repo.stargazersLink,
89 | forks: repo.forks,
90 | networkLink: repo.networkLink,
91 | language: repo.language
92 | }
93 | );
94 | htmlBox = htmlBox.replace(/(\r\n|\n|\r)/gm, "");
95 | return htmlBox;
96 | }
97 |
98 | function getCss(){
99 | let css = new CleanCSS().minify(['assets/css/github.repocard.css']).styles;
100 | css = ``;
101 | return css;
102 | }
103 |
--------------------------------------------------------------------------------
/posts/2022-01-01-edit-the-metadata.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Set up the metadata.json
3 | description: Setting up and configuring this theme.
4 | ---
5 |
6 | The [metadata.json file](https://github.com/mendhak/eleventy-satisfactory/blob/main/_data/metadata.json) contains various configuration items that are needed to set up the blog. It controls things like title, URLs, tags, feeds, and some links. The `metadata.json` file is located under [`_data/metadata.json`](https://github.com/mendhak/eleventy-satisfactory/blob/main/_data/metadata.json)
7 |
8 | Starting with the important ones.
9 |
10 | ### title
11 |
12 | The site title that appears in the header. eg, "Joe's Blog"
13 |
14 | {% notice "info" %}
15 | Current value: `{{ metadata.title | safe }}`
16 | {% endnotice %}
17 |
18 | ### pathPrefix
19 |
20 | Very important. The path where the blog will sit under its domain.
21 |
22 | Examples:
23 |
24 | - `/blog/` if it's hosted on 'https:\//example.com/blogs'
25 | - `/` if it's hosted on 'https:\//example.com'
26 |
27 | {% notice "info" %}
28 | Current value: `{{ metadata.pathPrefix }}`
29 | {% endnotice %}
30 |
31 | ### url
32 |
33 | The Base URL where this site will be published. It gets used to construct the full URL to content, in the [sitemap.xml](/sitemap.xml), [feed.xml](/feed.xml), [feed.json](/feed.json), and the OpenGraph and JSON-LD headers in the HTML ``.
34 |
35 | Examples:
36 |
37 | * `https://username.github.io/`
38 | * `https://example.com/blog/`
39 |
40 | {% notice "info" %}
41 | Current value: `{{ metadata.url }}`
42 | {% endnotice %}
43 |
44 |
45 | This value gets used to construct and correct various URLs throughout all content, such as images, links, stylesheets, etc.
46 |
47 |
48 | ## Optional, but useful
49 |
50 | ### paginationSize
51 |
52 | How many post links should be shown per page.
53 |
54 | {% notice "info" %}
55 | Current value: `{{ metadata.paginationSize }}`
56 | {% endnotice %}
57 |
58 | ### description
59 |
60 | Sets the site description in the HTML head's meta and OpenGraph/JSON-LD descriptions, as well as the JSON and Atom feed.
61 | If a blog post doesn't contain a `description:` frontmatter, this value gets used as a default in the HTML head.
62 |
63 | {% notice "info" %}
64 | Current value: `{{ metadata.description }}`
65 | {% endnotice %}
66 |
67 | ### tags
68 |
69 | Sets the site's tags in the HTML head's meta keywords and OpenGraph/JSON-LD tags.
70 | If a blog post doesn't contain a `tags:` frontmatter, these values get used as a default in the HTML head.
71 |
72 | {% notice "info" %}
73 | Current value: `{{ metadata.tags | dump | safe }}`
74 | {% endnotice %}
75 |
76 | ### language
77 |
78 | Sets the HTML `lang` of the web page. It helps search engines and browsers.
79 |
80 | {% notice "info" %}
81 | Current value: `{{ metadata.language }}`
82 | {% endnotice %}
83 |
84 | ### favicon
85 |
86 | Path to the favicon. Add your own image to the `/assets/images/` folder and update the location.
87 |
88 | {% notice "info" %}
89 | Current value: `{{ metadata.favicon }}`
90 | {% endnotice %}
91 |
92 | ### feed.path
93 |
94 | The path where the Atom feed should be written to.
95 |
96 | {% notice "info" %}
97 | Current value: `{{ metadata.feed.path }}`
98 | {% endnotice %}
99 |
100 | ### jsonfeed.path
101 |
102 | The path where the JSON feed should be written to.
103 |
104 | {% notice "info" %}
105 | Current value: `{{ metadata.jsonfeed.path }}`
106 | {% endnotice %}
107 |
108 | ### author
109 |
110 | `name`: The author pseudonym or name that should appear in the HTML meta as well as the site's footer.
111 | `email`: The author email that should appear in the Atom feed.
112 | `url`: The URL that should appear in the Atom feed.
113 |
114 | {% notice "info" %}
115 | Current value: `{{ metadata.author | dump | safe }}`
116 | {% endnotice %}
117 |
118 | ### opengraph
119 |
120 | Some opengraph settings.
121 |
122 | `image`: The image to use when an OpenGraph preview is being made for a URL on this site. If a blog post doesn't have a `opengraph.image` frontmatter, then this default image gets used instead. This value is also used in the JSON-LD data.
123 | `type`: The OpenGraph content type of this content. It's a blog so, it's an article.
124 |
125 | {% notice "info" %}
126 | Current value: `{{ metadata.opengraph | dump | safe }}`
127 | {% endnotice %}
128 |
129 |
130 |
--------------------------------------------------------------------------------
/posts/2021-12-24-post-with-various-headings.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Showcase of various heading sizes
3 | description: From H1 to H5 and some blockquotes
4 | tags: xmas
5 | opengraph:
6 | image: https://live.staticflickr.com/1932/30454355997_287063f84b_q.jpg
7 | ---
8 |
9 | A few different heading levels from H2 to H6. H1 is already used by the post title. H2 to H4 get permalinks, but H5 and H6 don't.
10 |
11 | ## Malesuada fames
12 |
13 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Gravida in fermentum et sollicitudin ac orci phasellus egestas. Non tellus orci ac auctor augue mauris augue. Amet consectetur adipiscing elit duis tristique. Arcu ac tortor dignissim convallis aenean. A cras semper auctor neque vitae. Donec massa sapien faucibus et molestie ac. Platea dictumst vestibulum rhoncus est pellentesque. Viverra nam libero justo laoreet sit amet cursus sit. Amet volutpat consequat mauris nunc congue nisi. Cras ornare arcu dui vivamus arcu.
14 |
15 | Malesuada fames ac turpis egestas. Porta nibh venenatis cras sed felis eget velit aliquet. Sagittis id consectetur purus ut faucibus pulvinar elementum integer enim. Arcu odio ut sem nulla pharetra. Rutrum tellus pellentesque eu tincidunt tortor aliquam nulla facilisi. Sed vulputate mi sit amet mauris commodo quis imperdiet massa. Ut etiam sit amet nisl purus in mollis nunc. Elit scelerisque mauris pellentesque pulvinar pellentesque habitant morbi tristique senectus. Egestas egestas fringilla phasellus faucibus scelerisque eleifend. Etiam tempor orci eu lobortis elementum nibh tellus molestie nunc. Integer feugiat scelerisque varius morbi. Velit ut tortor pretium viverra suspendisse. Lectus quam id leo in vitae turpis. Consequat id porta nibh venenatis cras sed felis eget.
16 |
17 | ### Scelerisque eleifend
18 |
19 | Scelerisque eleifend donec pretium vulputate sapien nec sagittis aliquam malesuada.
20 |
21 | > Viverra justo nec ultrices dui sapien eget mi proin. Tortor at risus viverra adipiscing at in tellus. Elementum curabitur vitae nunc sed velit dignissim. Accumsan lacus vel facilisis volutpat est. Pellentesque eu tincidunt tortor aliquam nulla facilisi cras fermentum odio. Viverra ipsum nunc aliquet bibendum enim facilisis gravida neque. Turpis nunc eget lorem dolor sed viverra ipsum nunc. Cursus risus at ultrices mi tempus imperdiet nulla malesuada pellentesque. Donec et odio pellentesque diam.
22 |
23 | Tempus quam pellentesque nec nam aliquam. Fringilla est ullamcorper eget nulla facilisi etiam dignissim. Ullamcorper velit sed ullamcorper morbi tincidunt ornare massa eget. Tellus id interdum velit laoreet id donec ultrices tincidunt. Platea dictumst vestibulum rhoncus est pellentesque elit ullamcorper. Faucibus et molestie ac feugiat.
24 |
25 |
26 | #### Amet purus gravida
27 |
28 | Amet purus gravida quis blandit turpis cursus in hac habitasse. Risus feugiat in ante metus dictum at tempor commodo ullamcorper. Mus mauris vitae ultricies leo integer malesuada nunc. At varius vel pharetra vel. Sed viverra ipsum nunc aliquet. In fermentum posuere urna nec tincidunt praesent semper. Sed vulputate odio ut enim. Vel fringilla est ullamcorper eget nulla facilisi etiam dignissim. Etiam non quam lacus suspendisse faucibus interdum posuere lorem. Volutpat maecenas volutpat blandit aliquam etiam erat. Diam quis enim lobortis scelerisque fermentum dui faucibus in ornare. Vitae turpis massa sed elementum tempus egestas sed. Sed risus pretium quam vulputate dignissim suspendisse in est. Odio morbi quis commodo odio. Quis viverra nibh cras pulvinar. Velit egestas dui id ornare arcu odio ut sem nulla.
29 |
30 | > Quis enim lobortis scelerisque fermentum dui faucibus in ornare. Pellentesque nec nam aliquam sem et. Urna molestie at elementum eu facilisis sed odio morbi. Metus aliquam eleifend mi in nulla posuere sollicitudin aliquam ultrices. Gravida neque convallis a cras semper. Dui sapien eget mi proin sed. Dictumst vestibulum rhoncus est pellentesque elit ullamcorper dignissim cras tincidunt. Parturient montes nascetur ridiculus mus mauris vitae ultricies leo. Imperdiet proin fermentum leo vel orci porta non pulvinar neque. Elit ullamcorper dignissim cras tincidunt. Laoreet sit amet cursus sit amet. At quis risus sed vulputate odio ut. Massa tincidunt nunc pulvinar sapien et.
31 |
32 | ##### Amet purus gravida
33 |
34 | Only H1 to H4s get permalinks. Don't want to go overboard.
35 |
36 |
37 | ###### Amet purus gravida
38 |
39 | That's as far as we'll go.
40 |
--------------------------------------------------------------------------------
/_includes/layouts/base.njk:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{ title or metadata.title }}
8 |
9 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | {% if includeJsonLD %}
53 |
70 | {% endif %}
71 |
72 |
73 |
74 |
75 |
76 | {{ metadata.title }}
77 |
78 |
79 |
80 | {{ content | safe }}
81 |
82 |
83 |
99 |
100 |
101 | {# Detects if images are in the content, it will add the simplelightbox JS/CSS if needed. #}
102 | {% addLightBoxRefIfNecessary %}
103 |
104 |
105 |
106 |
107 |
108 |
--------------------------------------------------------------------------------
/posts/post-with-an-image.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Post with various images
3 | description: There are different ways to post images!
4 | tags:
5 | - eleventy
6 | - markdown
7 | - shortcode
8 | - images
9 | - lightbox
10 |
11 | opengraph:
12 | image: /assets/images/image001.jpg
13 |
14 | ---
15 |
16 | There are a few different ways to display an image in a post.
17 |
18 |
19 |
20 | ## Using regular Markdown syntax
21 |
22 | Using regular Markdown syntax is possible. The image gets rendered as a figure with a caption. Clicking the image displays it in a lightbox.
23 |
24 | ```
25 | {% raw %}{% endraw %}
26 | ```
27 |
28 | Which results in:
29 |
30 | 
31 |
32 |
33 | ## Using the `figure` shortcode
34 |
35 | The `figure` shortcode is similar to the above, but it has some extra features.
36 | It produces a figure with an image, and a figcaption that supports Markdown.
37 | There are some width options, and the lightbox can be disabled.
38 |
39 | Here is the code:
40 |
41 | ```
42 | {% raw %}{% figure "/assets/images/image001.jpg", "Your **caption**" %}{% endraw %}
43 | ```
44 |
45 | Here is the output:
46 |
47 | {% figure "/assets/images/image001.jpg", "Your **caption**" %}
48 |
49 |
50 | ### Width options
51 |
52 | The image width can also be set to `default`, `half`, `third`, or `unconstrained`. It's the third argument to pass to the shortcode.
53 |
54 |
55 | ```
56 | {% raw %}{% figure "/assets/images/image001.jpg", "Your **caption**", "third" %}{% endraw %}
57 | ```
58 |
59 | Here is the output:
60 |
61 | {% figure "/assets/images/image001.jpg", "Your **caption**", "third" %}
62 |
63 | ### Disable lightbox
64 |
65 | The lightbox can be disabled, it is the fourth argument to pass to the shortcode.
66 |
67 | ```
68 | {% raw %}{% figure "/assets/images/image001.jpg", "", "", false %}{% endraw %}
69 | ```
70 |
71 | Produces:
72 |
73 | {% figure "/assets/images/image001.jpg", "", "", false %}
74 |
75 |
76 | ### Unconstrained full width image
77 |
78 | The `unconstrained` width option will let the image render to its full width, across the entire page. The lightbox is disabled if the width is set to unconstrained.
79 |
80 | ```
81 | {% raw %}{% figure "https://live.staticflickr.com/65535/49241129673_0f0d5f2751_4k.jpg",
82 | "Photo credit [mendhak](https://www.flickr.com/photos/mendhak/49241129673/)",
83 | "unconstrained" %}{% endraw %}
84 | ```
85 |
86 | Produces:
87 |
88 | {% figure "https://live.staticflickr.com/65535/49241129673_0f0d5f2751_4k.jpg",
89 | "Photo credit [mendhak](https://www.flickr.com/photos/mendhak/49241129673/)",
90 | "unconstrained" %}
91 |
92 |
93 | ## Straight up HTML
94 |
95 | HTML can be directly used in Markdown. In this example no lightbox is produced.
96 |
97 | ```html
98 | {% raw %}
99 |
100 |
101 | An image served using HTML figure and figcaption
102 |
103 | {% endraw %}
104 | ```
105 |
106 | Which results in:
107 |
108 |
109 |
110 |
111 |
112 | An image served using HTML figure and figcaption
113 |
114 |
115 |
116 |
117 | ### ...with a lightbox
118 |
119 | Adding a hyperlink to the image will make it appear in a lightbox.
120 |
121 |
122 | ```html
123 | {% raw %}
124 |
125 |
126 |
127 |
128 | An image served using HTML figure and figcaption
129 |
130 | {% endraw %}
131 | ```
132 |
133 | Which results in:
134 |
135 |
136 |
137 |
138 |
139 |
140 | An image served using HTML figure and figcaption
141 |
142 |
143 |
144 |
145 | ### Standalone ``
146 |
147 | Using the `` HTML tag, the image is shown as-is, no lightbox, no center alignment.
148 |
149 | ```html
150 | {% raw %}{% endraw %}
151 | ```
152 |
153 | Produces:
154 |
155 |
156 |
157 |
--------------------------------------------------------------------------------
/assets/css/index.css:
--------------------------------------------------------------------------------
1 | /* Overriding simple.css values */
2 | /* Visible at file:///home/mendhak/Projects/eleventy-satisfactory/node_modules/simpledotcss/simple.css */
3 |
4 |
5 | :root {
6 | /* Set sans-serif & mono fonts */
7 | --sans-font: Roboto,"Nimbus Sans L","Avenir Next",Avenir,"Segoe UI",Arial,Helvetica,"Helvetica Neue",sans-serif;
8 | --mono-font: "Ubuntu Mono", Consolas, Menlo, Monaco, "Andale Mono", monospace;
9 | }
10 |
11 | /* Change some colors for dark mode */
12 | @media (prefers-color-scheme: dark) {
13 | :root {
14 | --bg: #333333;
15 | --accent: #fedb8b;
16 | }
17 | }
18 |
19 | /* Make the middle column slightly wider, and font slightly larger */
20 | body {
21 | grid-template-columns: 1fr min(55rem, 90%) 1fr;
22 | font-size: 1.25rem;
23 | line-height: 1.7;
24 | overflow-y: scroll; /* stops horizontal shift due to scrollbar */
25 | }
26 |
27 | /* Left align the site's title in the header */
28 | body > header {
29 | text-align: left;
30 | padding: 0;
31 | }
32 | /* Styling for site title */
33 | body > header > a {
34 | max-width: 1200px;
35 | margin: 1rem auto;
36 | font-size: 1.5rem;
37 | display: block;
38 | font-weight: 700;
39 | color: var(--text) !important;
40 | font-family: 'Lora';
41 | padding-left: 0.5rem;
42 | }
43 |
44 | /* @view-transition {
45 | navigation: auto;
46 | } */
47 |
48 |
49 | /* No underlines for hyperlinks */
50 | a {
51 | text-decoration: none;
52 | }
53 |
54 | /* Don't italicize blockquote */
55 | blockquote {
56 | font-style: normal;
57 | }
58 |
59 | /* H1s should have the accent color, to make it stand out */
60 | h1 {
61 | color: var(--accent);
62 | }
63 |
64 | /* Use the nice Lora font for headings */
65 | h1,h2,h3,h4,h5,h6 {
66 | font-family: Lora,serif;
67 | margin-bottom: 0.25em;
68 | }
69 |
70 |
71 |
72 | /* This class is added by the markdown-it-anchor library. Removing the hyperlink color from headers */
73 | a.header-anchor {
74 | color: unset;
75 | }
76 |
77 | /* Time text slightly grayed out */
78 | time {
79 | color: var(--text-light);
80 | font-size: 1rem;
81 | padding-top: 1.5rem;
82 | display: inline-block;
83 | }
84 |
85 | /* Get footer to span all 3 columns */
86 | footer {
87 | grid-column: 1/-1;
88 | background-color: var(--accent-bg);
89 | padding-top: 3rem;
90 | box-shadow: 0 50vh 0 50vh var(--accent-bg);
91 | }
92 |
93 | /* Don't show bullet points for lists in footers */
94 | footer ul {
95 | margin: 0;
96 | padding: 0;
97 | list-style-type: none;
98 | }
99 |
100 | footer li {
101 | display: inline-block;
102 | padding-top: 5px;
103 | padding-bottom: 5px;
104 | }
105 |
106 | /* Styling for the excerpts appear below article titles on the index page */
107 | .excerpt {
108 | font-size: 1.2rem;
109 | padding-bottom: 1.2rem;
110 | }
111 |
112 | /* Styling for the article, which wraps the post body */
113 | article {
114 | border: none;
115 | padding: 0;
116 | padding-bottom: 2rem;
117 | }
118 |
119 | /* article a {
120 | font-weight: bolder;
121 | font-family: 'Lora';
122 | font-size: 1.5rem;
123 | } */
124 |
125 | /* Styling for the external links appearing in the footer */
126 | .bottomlinks {
127 | font-variant: small-caps;
128 | font-weight: bolder;
129 | font-size: 1rem;
130 | }
131 |
132 | /* Styling for the photos appearing in the footer */
133 | .photostream {
134 | padding-top: 2rem;
135 | padding-bottom: 2rem;
136 | }
137 |
138 | /* Simple lightbox overlay */
139 | .sl-overlay {
140 | background: black !important;
141 | }
142 |
143 |
144 |
145 | /* Navigation for Pagination */
146 |
147 | nav {
148 | display: inline;
149 | text-align: center;
150 | margin-top: 2rem;
151 | width: 100%;
152 |
153 | }
154 |
155 | nav :is(ol,ul) li {
156 | display: inline;
157 | list-style-type: none;
158 | padding: 0.2rem 0.5rem;
159 | text-decoration: none;
160 | margin: 0 0.5rem;
161 | float: right;
162 |
163 | }
164 |
165 | /* Alert info warning error panels */
166 | :root {
167 | --notice-color: var(--text);
168 | --notice-bg: var(--accent-bg);
169 | --notice-info-color: var(--text);
170 | --notice-info-bg: #d8ebf1;
171 | --notice-success-color: var(--text);
172 | --notice-success-bg: #d9edd9;
173 | --notice-warning-color: var(--text);
174 | --notice-warning-bg: #f7e5cd;
175 | --notice-danger-color: var(--text);
176 | --notice-danger-bg: #fcdfde;
177 | }
178 |
179 | @media (prefers-color-scheme: dark) {
180 | :root {
181 | --notice-color: var(--text);
182 | --notice-bg: var(--accent-bg);
183 | --notice-info-color: #76a9fa;
184 | --notice-info-bg: var(--accent-bg);
185 | --notice-success-color: #31c48d;
186 | --notice-success-bg: var(--accent-bg);
187 | --notice-warning-color: #faca15;
188 | --notice-warning-bg: var(--accent-bg);
189 | --notice-danger-color: #f98080;
190 | --notice-danger-bg: var(--accent-bg);
191 | }
192 | }
193 |
194 | .notice {
195 | background: var(--accent-bg);
196 | border-radius: 5px;
197 | padding: 1rem;
198 | margin: 2rem 0;
199 | font-size: 1rem;
200 | }
201 |
202 | .notice a {
203 | color: unset;
204 | text-decoration: underline;
205 | }
206 |
207 | .notice.info {
208 | color: var(--notice-info-color);
209 | background-color: var(--notice-info-bg);
210 | }
211 |
212 | .notice.success {
213 | color: var(--notice-success-color);
214 | background-color: var(--notice-success-bg);
215 | }
216 |
217 | .notice.warning {
218 | color: var(--notice-warning-color);
219 | background-color: var(--notice-warning-bg);
220 | }
221 |
222 | .notice.danger {
223 | color: var(--notice-danger-color);
224 | background-color: var(--notice-danger-bg);
225 | }
226 |
227 |
228 | .gist td {
229 | all: initial;
230 | }
231 |
232 | /* Video */
233 |
234 | div.video > iframe {
235 | aspect-ratio: 16 / 9;
236 | width: 100%;
237 | }
238 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Eleventy Satisfactory Blog Theme
2 | [](https://github.com/mendhak/eleventy-satisfactory/actions/workflows/staticsite.yml) [](https://github.com/mendhak/eleventy-satisfactory/actions/workflows/playwright.yml)
3 |
4 | An Eleventy blog theme focused on content, and various convenience features. [Demo](https://code.mendhak.com/eleventy-satisfactory/).
5 |
6 |
7 | |||
8 | |:--|:--|
9 |
10 | Makes use of SimpleCSS, with inspiration from Hylia and minimal-mistakes. I developed it for [my blog](https://code.mendhak.com).
11 |
12 | **Features**
13 |
14 | * My focus is mainly on content, and less on everything else (author, social, tags, etc).
15 | * Images using regular Markdown syntax are displayed with lightbox
16 | * Image gallery with captions
17 | * Link to a post's .md file is converted to final URL
18 | * Links and images paths automatically adjusted to match blog prefix
19 | * Main CSS is inline in the page
20 | * Automatic light and dark mode
21 | * Simplified post layout with date at bottom
22 | * Simplified home page layout
23 | * Adjust number of post listings
24 | * Post excerpts below post listings
25 | * Links, year, copyright, and photos in footer
26 | * Opengraph and JSON+LD metadata for posts including preview image
27 | * Code blocks rendered using Prism syntax highlighting
28 | * Notice boxes such as info, warning, success
29 | * Seamless gist integration as code blocks
30 | * Github repo cards generator
31 | * No tags listings, no site navigation, no social, no landing page, no analytics
32 | * Draft posts appear locally
33 | * UI testing (for this repo)
34 | * Github Actions for publishing
35 | * Extra wide images, videos and code blocks to make it extra-wide and stand out from the text
36 | * Full width images and videos
37 | * Robots.txt, .well-known folder
38 | * Favicon
39 |
40 |
41 | ## How to use this blog theme
42 |
43 | Generate a repo from this public template, or fork it.
44 | Delete the `tests/` folder and `.github/workflows/playwright.yml`
45 |
46 | ### Configuration
47 |
48 | - [Set your values in the metadata.json file](https://code.mendhak.com/eleventy-satisfactory/edit-the-metadata/)
49 | - [Set the footer links and images](https://code.mendhak.com/eleventy-satisfactory/set-footer-links/)
50 |
51 |
52 | ### Write a post
53 |
54 | The posts go in the `posts` folder. Drafts can go in the `drafts` folder and will only appear when running locally (`npm run start` or `docker compose up`) but not when building (`npm run build`).
55 |
56 | - [Set the date of a post](https://code.mendhak.com/eleventy-satisfactory/set-date-of-post/)
57 | - [Add an image](https://code.mendhak.com/eleventy-satisfactory/post-with-an-image/)
58 | - [Add an image gallery](https://code.mendhak.com/eleventy-satisfactory/post-with-a-gallery/)
59 | - [Link to another post or URL](https://code.mendhak.com/eleventy-satisfactory/posting-links/)
60 | - [Add a notice panel with info, warning, success, danger](https://code.mendhak.com/eleventy-satisfactory/post-notice/)
61 | - [Add a code block with syntax highlighting](https://code.mendhak.com/eleventy-satisfactory/post-with-code/)
62 | - [Add a Github Gist](https://code.mendhak.com/eleventy-satisfactory/post-with-github-gists/)
63 | - [Add a Github Repo Card](https://code.mendhak.com/eleventy-satisfactory/github-repo-card/)
64 | - [Add videos and audio](https://code.mendhak.com/eleventy-satisfactory/post-with-iframes-videos-third-party/)
65 | - [Controlling extra-wide images, videos, code blocks](https://code.mendhak.com/eleventy-satisfactory/extra-wide-full-width-images-videos/)
66 | - [Set Opengraph preview image and metadata](https://code.mendhak.com/eleventy-satisfactory/opengraph-preview-data/)
67 |
68 |
69 | Publish it. I've included a [sample Github Action](.github/workflows/staticsite.yml), use or modify as you need.
70 |
71 |
72 | ## How to run the blog locally
73 |
74 | **Running it with Docker**
75 |
76 | This will do the npm install and npm start together.
77 |
78 | ```
79 | docker compose up
80 | ```
81 |
82 | Then browse to http://localhost:8080/
83 |
84 |
85 | **Running it with Node**
86 |
87 | Requires Node 20. First get all the dependencies.
88 |
89 | ```
90 | npm install
91 | ```
92 |
93 | To serve the site, and watch for changes:
94 |
95 | ```
96 | npm run start
97 | ```
98 |
99 | Then browse to http://localhost:8080/
100 |
101 |
102 | To just build the site once (normally used for Github Actions):
103 |
104 | ```
105 | npm install
106 | npm run build
107 | ```
108 |
109 | **Running Tests**
110 |
111 | Make sure Firefox is installed. `sudo apt install firefox`
112 |
113 | Run Playwright tests, this should automatically do an npm start first.
114 |
115 | ```
116 | npm test
117 | ```
118 |
119 | ## Updating
120 |
121 | Once you've forked this repo, copying new features and updates from this repo to yours shouldn't be done wholesale. Instead, a diff tool like meld or Beyond Compare is recommended.
122 |
123 | For commandline, here's a useful rsync command which copies most files and excludes some unnecessary ones. At least it's a starting point:
124 |
125 | ```
126 | cd your-blog-repo
127 | rsync -av --progress ../eleventy-satisfactory/ ./ \
128 | --exclude '.git' \
129 | --exclude 'tests' \
130 | --exclude '_site' \
131 | --exclude 'node_modules' \
132 | --exclude 'posts' --include 'posts/posts.json' \
133 | --exclude 'drafts/*.md' --include 'drafts.11tydata.js' --include 'drafts.json' \
134 | --exclude 'playwright*' \
135 | --exclude 'test*' \
136 | --exclude '_data' \
137 | --exclude 'screenshots' \
138 | --exclude 'README.md'
139 | ```
140 |
141 | After the rsync, it's still worth doing a git diff to see what's changed, or if there are any conflicts.
142 |
143 |
144 | ## Notes
145 |
146 | **TODO/Investigate**
147 |
148 | - Can the cssmin shortcode adjust the inline url() paths? That could allow putting the font lines into its own CSS file.
149 | - Can the Github CSS be included just once, instead of once per card? This could be done using WebC, have to wrap my head around it.
150 | - Should the gallery behave the same for gallery + markdown and gallery + shortcode?
151 | - There isn't a _clean_ way to give users the ability to [toggle light and dark mode](https://github.com/mendhak/eleventy-satisfactory/issues/2)
152 |
153 |
154 | **References**
155 |
156 | - [Eleventy](https://www.11ty.dev/docs/)
157 | - [Simple.css](https://github.com/kevquirk/simple.css/wiki)
158 | - [Favicon created by Uniconlabs](https://www.flaticon.com/free-icons/website)
159 | - [Article icon by Freepik](https://www.flaticon.com/free-icons/blog)
160 | - [Paired shortcode](https://www.markllobrera.com/posts/eleventy-paired-shortcodes-and-markdown-rendering/)
161 | - [Shortcode with image gallery](https://www.markllobrera.com/posts/eleventy-building-image-gallery-photoswipe/)
162 | - [Customizing markdown-it](https://publishing-project.rivendellweb.net/customizing-markdown-it/)
163 |
164 |
165 |
--------------------------------------------------------------------------------
/posts/customary-lorem-ipsum.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Customary Lorem Ipsum post
3 | description: This is a post on My Blog with a wall of text.
4 | date: 2018-05-01
5 | tags:
6 | - another tag
7 |
8 | ---
9 |
10 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Tempor orci eu lobortis elementum nibh tellus molestie nunc non. Amet facilisis magna etiam tempor orci eu lobortis elementum nibh. Pharetra sit amet aliquam id. Volutpat odio facilisis mauris sit amet massa vitae tortor condimentum. Semper viverra nam libero justo laoreet. Mattis rhoncus urna neque viverra justo nec ultrices dui. Mauris augue neque gravida in fermentum et sollicitudin. Nunc mattis enim ut tellus elementum sagittis vitae. Nibh sed pulvinar proin gravida hendrerit lectus. Diam quam nulla porttitor massa. Ac felis donec et odio pellentesque diam volutpat. Cursus eget nunc scelerisque viverra mauris in. Non curabitur gravida arcu ac tortor dignissim convallis. Adipiscing at in tellus integer feugiat.
11 |
12 | ## Consectetur libero id
13 |
14 | Faucibus nisl tincidunt eget nullam non nisi. Tristique nulla aliquet enim tortor at auctor urna. Nisi scelerisque eu ultrices vitae auctor eu augue ut. Cras ornare arcu dui vivamus arcu felis bibendum ut. Eget nulla facilisi etiam dignissim diam quis enim. Consectetur purus ut faucibus pulvinar elementum. Ultricies tristique nulla aliquet enim tortor at auctor urna nunc. Ullamcorper velit sed ullamcorper morbi tincidunt ornare. Id semper risus in hendrerit gravida rutrum. Dignissim convallis aenean et tortor at.
15 |
16 | 
17 |
18 | Laoreet non curabitur gravida arcu. Neque sodales ut etiam sit amet. Vitae purus faucibus ornare suspendisse sed nisi lacus sed viverra. Ornare arcu dui vivamus arcu. Et leo duis ut diam quam nulla porttitor massa id. Diam sit amet nisl suscipit adipiscing bibendum est. Porttitor massa id neque aliquam vestibulum morbi blandit. Tellus integer feugiat scelerisque varius morbi enim nunc faucibus. Dui ut ornare lectus sit amet est placerat in egestas. Vel elit scelerisque mauris pellentesque pulvinar.
19 |
20 |
21 |
22 | Sit amet justo donec enim diam vulputate ut pharetra sit. Mi proin sed libero enim sed faucibus. Tortor at auctor urna nunc id cursus metus. In pellentesque massa placerat duis ultricies. Semper feugiat nibh sed pulvinar.
23 |
24 | Amet purus gravida quis blandit turpis cursus in hac. Natoque penatibus et magnis dis parturient montes nascetur. Tortor dignissim convallis aenean et tortor at. Eget est lorem ipsum dolor. Lectus nulla at volutpat diam ut venenatis tellus. Ultrices neque ornare aenean euismod elementum nisi quis eleifend. At tellus at urna condimentum mattis pellentesque id. Egestas egestas fringilla phasellus faucibus scelerisque eleifend. Etiam tempor orci eu lobortis elementum nibh tellus. Integer enim neque volutpat ac.
25 |
26 | ### Non blandit massa enim nec
27 |
28 | Leo urna molestie at elementum eu facilisis. Vestibulum sed arcu non odio. Tortor aliquam nulla facilisi cras fermentum odio. Ante in nibh mauris cursus mattis molestie. Lacus sed viverra tellus in. Tortor pretium viverra suspendisse potenti nullam ac tortor vitae purus. Non consectetur a erat nam at lectus urna duis convallis. Tortor dignissim convallis aenean et tortor at risus viverra adipiscing. Tincidunt id aliquet risus feugiat in ante. Aliquam vestibulum morbi blandit cursus risus at ultrices mi tempus. Nec ultrices dui sapien eget. Ullamcorper a lacus vestibulum sed arcu. Et odio pellentesque diam volutpat commodo sed egestas. Orci phasellus egestas tellus rutrum tellus pellentesque eu tincidunt. Ipsum a arcu cursus vitae. Adipiscing elit duis tristique sollicitudin nibh sit. Quis ipsum suspendisse ultrices gravida dictum fusce ut placerat orci. Felis donec et odio pellentesque diam volutpat commodo. Eget nulla facilisi etiam dignissim diam quis.
29 |
30 | > Mauris a diam maecenas sed enim ut sem viverra. Suspendisse faucibus interdum posuere lorem ipsum dolor sit amet. Cursus eget nunc scelerisque viverra. Id velit ut tortor pretium viverra suspendisse. Ultrices eros in cursus turpis. Quis hendrerit dolor magna eget est lorem ipsum dolor. Diam in arcu cursus euismod quis. Vitae suscipit tellus mauris a diam maecenas sed. Purus non enim praesent elementum facilisis leo vel. Porttitor massa id neque aliquam. Lectus urna duis convallis convallis tellus. Nunc consequat interdum varius sit amet mattis vulputate enim.
31 |
32 | Orci nulla pellentesque dignissim enim sit. Tincidunt vitae semper quis lectus nulla at volutpat. Molestie at elementum eu facilisis. In aliquam sem fringilla ut morbi tincidunt augue interdum. Dui nunc mattis enim ut tellus elementum. Ultricies lacus sed turpis tincidunt id aliquet risus feugiat in. Enim diam vulputate ut pharetra sit amet aliquam id. Volutpat est velit egestas dui id ornare. Eu mi bibendum neque egestas congue. Cras sed felis eget velit. Integer quis auctor elit sed vulputate mi sit amet mauris. Gravida rutrum quisque non tellus orci. Rutrum quisque non tellus orci ac .
33 |
34 | ### Tincidunt arcu non sodales
35 |
36 | Neque sodales ut etiam sit. Nibh venenatis cras sed felis eget velit aliquet sagittis id. Aliquet enim tortor at auctor urna nunc id. Turpis massa tincidunt dui ut ornare lectus sit amet. Id faucibus nisl tincidunt eget nullam non. Vitae ultricies leo integer malesuada nunc. Volutpat est velit egestas dui id ornare arcu odio ut. Cursus turpis massa tincidunt dui ut ornare. Neque aliquam vestibulum morbi blandit cursus risus at. Aenean vel elit scelerisque mauris. Quisque sagittis purus sit amet volutpat consequat mauris nunc congue. Nibh tortor id aliquet lectus proin. Sagittis nisl rhoncus mattis rhoncus urna neque.
37 |
38 | ```java
39 | private void loremFunction(boolean ipsum) {
40 | LOG.debug("Lorem ipsum begins");
41 | Intent sendIntent = new Intent();
42 | sendIntent.putExtra("lorem ipsum dolor", "sit amet");
43 | }
44 | ```
45 |
46 | Metus vulputate eu scelerisque felis imperdiet proin fermentum leo. Vulputate enim nulla aliquet porttitor lacus luctus accumsan tortor posuere. Vel pharetra vel turpis nunc eget lorem dolor. Ut tellus elementum sagittis vitae et. Donec ultrices tincidunt arcu non sodales neque sodales. Dis parturient montes nascetur ridiculus mus mauris vitae. Pulvinar etiam non quam lacus suspendisse. Senectus et netus et malesuada. Sed viverra tellus in hac habitasse. Est sit amet facilisis magna etiam tempor orci eu lobortis.
47 |
48 | Ac turpis egestas integer eget. Euismod elementum nisi quis eleifend. Risus quis varius quam quisque id diam. Adipiscing elit ut aliquam purus sit amet. Tristique senectus et netus et malesuada fames ac. Fames ac turpis egestas sed tempus urna et pharetra. Vel pharetra vel turpis nunc eget lorem dolor. Augue eget arcu dictum varius duis at. Nibh tortor id aliquet lectus proin nibh.
49 |
50 | ## Bibendum ut tristique
51 |
52 | Et egestas quis ipsum suspendisse ultrices gravida. Est placerat in egestas erat imperdiet. Imperdiet dui accumsan sit amet nulla facilisi. Quis ipsum suspendisse ultrices gravida dictum fusce ut placerat orci. Aliquet risus feugiat in ante metus dictum at tempor. Vel pharetra vel turpis nunc eget lorem. Suscipit tellus mauris a diam maecenas. Volutpat commodo sed egestas egestas fringilla phasellus faucibus. Justo eget magna fermentum iaculis. Sed euismod nisi porta lorem mollis aliquam ut porttitor leo. Gravida rutrum quisque non tellus orci. Cursus metus aliquam eleifend mi in. Vulputate mi sit amet mauris commodo.
53 |
54 | Arcu odio ut sem nulla pharetra diam sit amet. Ipsum faucibus vitae aliquet nec ullamcorper sit. At imperdiet dui accumsan sit amet nulla facilisi. In nibh mauris cursus mattis molestie a iaculis at erat. Suspendisse in est ante in nibh. Fringilla phasellus faucibus scelerisque eleifend donec pretium vulputate sapien nec. Elit duis tristique sollicitudin nibh. Sagittis nisl rhoncus mattis rhoncus urna neque. Sit amet volutpat consequat mauris nunc. Eget nullam non nisi est sit amet. Et leo duis ut diam quam. Tincidunt arcu non sodales neque sodales ut. Auctor elit sed vulputate mi sit amet. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus. Cras adipiscing enim eu turpis. Placerat in egestas erat imperdiet. Egestas tellus rutrum tellus pellentesque eu tincidunt tortor.
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/.eleventy.js:
--------------------------------------------------------------------------------
1 | import "dotenv/config";
2 | import markdownIt from "markdown-it";
3 | import markdownItAnchor from "markdown-it-anchor";
4 |
5 | import pluginRss from "@11ty/eleventy-plugin-rss";
6 | import pluginSyntaxHighlight from "@11ty/eleventy-plugin-syntaxhighlight";
7 |
8 | import { readFile } from 'fs/promises';
9 | const metadata = JSON.parse(
10 | await readFile(
11 | new URL('./_data/metadata.json', import.meta.url)
12 | )
13 | );
14 |
15 | // Change this to match the actual path prefix.
16 | const pathPrefix = process.env.PATH_PREFIX || metadata.pathPrefix;
17 |
18 |
19 | import { InputPathToUrlTransformPlugin, EleventyHtmlBasePlugin } from "@11ty/eleventy";
20 | import { DateTime } from "luxon";
21 |
22 | // Various custom shortcodes and filters
23 | import imageRenderer from "./_configs/markdownlibrary.renderer.image.js";
24 | import cssminification from './_configs/cssminification.shortcode.js';
25 | import notice from './_configs/notice.shortcode.js';
26 | import button from './_configs/button.shortcode.js';
27 | import figure from './_configs/figure.shortcode.js';
28 | import lightbox from './_configs/lightboxref.shortcode.js';
29 | import gallery from './_configs/gallery.shortcode.js';
30 | import video from './_configs/video.shortcode.js';
31 | import excerpt from './_configs/excerpt.shortcode.js';
32 | import ghRepoCard from './_configs/githubrepocard.shortcode.js';
33 | import gist from './_configs/gist.shortcode.js';
34 |
35 |
36 | /** @param {import("@11ty/eleventy").UserConfig} eleventyConfig */
37 | export default async function (eleventyConfig) {
38 |
39 | // Copy the `assets` (includes images, fonts) folders to the output
40 | eleventyConfig.addPassthroughCopy("assets/fonts");
41 | eleventyConfig.addPassthroughCopy("assets/images");
42 | eleventyConfig.addPassthroughCopy({"assets/.well-known": ".well-known"});
43 | eleventyConfig.addPassthroughCopy({ "node_modules/simplelightbox/dist/simple-lightbox.min.css": "simplelightbox/simple-lightbox.min.css" });
44 | eleventyConfig.addPassthroughCopy({ "node_modules/simplelightbox/dist/simple-lightbox.min.js": "simplelightbox/simple-lightbox.min.js" });
45 |
46 | //Since moving the CSS inline eleventy no longer watches it (because it's not being copied to output), so I had to include it as a watch target.
47 | //Adding it to addPassthroughCopy also means it's not watched.
48 | eleventyConfig.addWatchTarget("assets/css/");
49 | eleventyConfig.addWatchTarget("assets/js/");
50 |
51 | // Customize Markdown library and settings:
52 | let markdownLibrary = markdownIt({
53 | html: true,
54 | linkify: false,
55 | typographer: true
56 | }).use(markdownItAnchor, {
57 | permalink: markdownItAnchor.permalink.headerLink(),
58 | level: [1, 2, 3, 4],
59 | slugify: eleventyConfig.getFilter("slugify")
60 | });
61 |
62 | // Wrap images in a figure, a, and figcaption.
63 | // This lets the simplelightbox code serve it up too!
64 | // Also adds loading lazy attribute
65 | markdownLibrary.renderer.rules.image = (tokens, idx, options, env, slf) => imageRenderer(tokens, idx, options, env, slf, markdownLibrary);
66 |
67 |
68 | eleventyConfig.setLibrary("md", markdownLibrary);
69 | // Re-enable the indented code block feature
70 | eleventyConfig.amendLibrary("md", mdLib => mdLib.enable("code"))
71 |
72 | // RSS
73 | eleventyConfig.addPlugin(pluginRss);
74 | // Code syntax with Prism JS
75 | eleventyConfig.addPlugin(pluginSyntaxHighlight);
76 |
77 | //Converts most URLs to URLs with pathPrefix
78 | eleventyConfig.addPlugin(EleventyHtmlBasePlugin);
79 | // Automatically convert links pointing at .md to corresponding URL.
80 | eleventyConfig.addPlugin(InputPathToUrlTransformPlugin);
81 |
82 | // Date used below posts
83 | eleventyConfig.addFilter("readableDate", dateObj => {
84 | return DateTime.fromJSDate(dateObj, { zone: 'utc' }).toFormat("dd LLL yyyy");
85 | });
86 |
87 | // Date used in sitemap and data attribute
88 | // https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-date-string
89 | eleventyConfig.addFilter('htmlDateString', (dateObj) => {
90 | return DateTime.fromJSDate(dateObj, { zone: 'utc' }).toFormat('yyyy-LL-dd');
91 | });
92 |
93 | // Get the first `n` elements of a collection. Used on the home page to limit number of items to display.
94 | eleventyConfig.addFilter("head", (array, n) => {
95 | if (!Array.isArray(array) || array.length === 0) {
96 | return [];
97 | }
98 | if (n < 0) {
99 | return array.slice(n);
100 | }
101 |
102 | return array.slice(0, n);
103 | });
104 |
105 | // Ensures that draft postts only show up when doing a --watch or --serve
106 | // https://www.11ty.dev/docs/config-preprocessors/#example-drafts
107 | eleventyConfig.addPreprocessor("drafts", "*", (data, content) => {
108 | if (data.draft && process.env.ELEVENTY_RUN_MODE === "build") {
109 | return false;
110 | }
111 | });
112 |
113 | // Filters out irrelevant tags that aren't really related to content, only used for organising things
114 | eleventyConfig.addFilter("filterTagList", function (tags) {
115 | return (tags || []).filter(tag => ["all", "nav", "post", "posts"].indexOf(tag) === -1);
116 | });
117 |
118 | // filter to convert content to Markdown.
119 | // Useful for allowing `code` in the h1
120 | eleventyConfig.addFilter("markdown", (content) => {
121 | return markdownLibrary.renderInline(content);
122 | });
123 |
124 | // Paired shortcode that takes a JSON array of CSS file paths
125 | // It then combines them, which includes reconciles overriden values!
126 | // And returns the output.
127 | eleventyConfig.addPairedShortcode("cssminification", cssminification);
128 |
129 | //Paired shortcode to display a notice panel like standard, error, warning, etc.
130 | eleventyConfig.addPairedShortcode("notice", (data, noticeType) => notice(data, noticeType, markdownLibrary));
131 |
132 | //Shortcode to render a button, optionally with a link
133 | eleventyConfig.addShortcode("button", button);
134 |
135 | // Paired shortcode to display a figure with caption.
136 | // This is very similar to the regular Markdown image,
137 | // But I'll keep this around in case the other way ever breaks in the future
138 | // Plus, this has the 'width' flexibility, and maybe more future features.
139 | eleventyConfig.addShortcode("figure", (image, caption, widthName, useLightbox=true) => figure(image, caption, widthName, useLightbox, markdownLibrary));
140 |
141 | // If the post contains images, then add the Lightbox JS/CSS and render lightboxes for it.
142 | // Since it needs access to the `page` object, we can't use arrow notation here.
143 | eleventyConfig.addShortcode("addLightBoxRefIfNecessary", function () { return lightbox(this.page); });
144 |
145 | // The `gallery` paired shortcode shows a set of images and displays it in a row together.
146 | eleventyConfig.addPairedShortcode("gallery", (data, caption) => gallery(data, caption, markdownLibrary));
147 |
148 |
149 | // The `video` shortcode gets a YouTube video and displays it
150 | eleventyConfig.addShortcode("video", video);
151 |
152 | // Generate excerpt from first paragraph
153 | eleventyConfig.addShortcode("excerpt", excerpt);
154 |
155 | // Show the current year using a shortcode
156 | eleventyConfig.addShortcode("year", () => `${new Date().getFullYear()}`);
157 |
158 | // Shortcode for Github Repo Card
159 | eleventyConfig.addShortcode("githubrepocard", ghRepoCard);
160 |
161 | // The `gist` shortcode renders the gist's files as code blocks
162 | // For some reason calling the method directly isn't possible, I have to wrap it.
163 | eleventyConfig.addShortcode("gist", async (gistId) => gist(gistId, markdownLibrary));
164 |
165 |
166 | return {
167 | // Control which files Eleventy will process
168 | // e.g.: *.md, *.njk, *.html, *.liquid
169 | templateFormats: [
170 | "md",
171 | "njk",
172 | "html",
173 | "liquid"
174 | ],
175 |
176 | // Pre-process *.md files with: (default: `liquid`)
177 | markdownTemplateEngine: "njk",
178 |
179 | // Pre-process *.html files with: (default: `liquid`)
180 | htmlTemplateEngine: "njk",
181 |
182 | // -----------------------------------------------------------------
183 | // If your site deploys to a subdirectory, change `pathPrefix` in metadata.json.
184 | // Don’t worry about leading and trailing slashes, we normalize these.
185 |
186 | // If you don’t have a subdirectory, use "" or "/" (they do the same thing)
187 | // This is only used for link URLs (it does not affect your file structure)
188 | // Best paired with the `url` filter: https://www.11ty.dev/docs/filters/url/
189 |
190 | // You can also pass this in on the command line using `--pathprefix`
191 |
192 | // Optional (default is "/")
193 | pathPrefix: pathPrefix,
194 | // -----------------------------------------------------------------
195 |
196 | // These are all optional (defaults are shown):
197 | dir: {
198 | input: ".",
199 | includes: "_includes",
200 | data: "_data",
201 | output: "_site"
202 | }
203 | };
204 | };
205 |
--------------------------------------------------------------------------------