chore: apply automatic linting

This commit is contained in:
Corbin Crutchley
2022-09-25 05:51:04 -07:00
parent 5d628203d3
commit 3900a13481
92 changed files with 4787 additions and 4694 deletions

View File

@@ -1,46 +1,46 @@
const tsRules = { const tsRules = {
"@typescript-eslint/ban-types": "off", "@typescript-eslint/ban-types": "off",
"@typescript-eslint/no-empty-interface": "off" "@typescript-eslint/no-empty-interface": "off",
} };
module.exports = { module.exports = {
env: { env: {
node: true, node: true,
browser: true, browser: true,
}, },
extends: ['eslint:recommended', 'plugin:astro/recommended'], extends: ["eslint:recommended", "plugin:astro/recommended"],
parserOptions: { parserOptions: {
ecmaVersion: 'latest', ecmaVersion: "latest",
sourceType: 'module', sourceType: "module",
}, },
rules: { rules: {
"no-unused-vars": "off" "no-unused-vars": "off",
}, },
overrides: [ overrides: [
{ {
files: ['*.astro'], files: ["*.astro"],
parser: 'astro-eslint-parser', parser: "astro-eslint-parser",
parserOptions: { parserOptions: {
parser: '@typescript-eslint/parser', parser: "@typescript-eslint/parser",
extraFileExtensions: ['.astro'], extraFileExtensions: [".astro"],
}, },
rules: { rules: {
...tsRules ...tsRules,
}, },
}, },
{ {
files: ['*.ts'], files: ["*.ts"],
parser: '@typescript-eslint/parser', parser: "@typescript-eslint/parser",
extends: ['plugin:@typescript-eslint/recommended'], extends: ["plugin:@typescript-eslint/recommended"],
rules: { rules: {
...tsRules ...tsRules,
}, },
}, },
{ {
// Define the configuration for `<script>` tag. // Define the configuration for `<script>` tag.
// Script in `<script>` is assigned a virtual file name with the `.js` extension. // Script in `<script>` is assigned a virtual file name with the `.js` extension.
files: ['**/*.astro/*.js', '*.astro/*.js'], files: ["**/*.astro/*.js", "*.astro/*.js"],
parser: '@typescript-eslint/parser', parser: "@typescript-eslint/parser",
}, },
], ],
}; };

View File

@@ -1,11 +1,11 @@
{ {
"useTabs": true, "useTabs": true,
"overrides": [ "overrides": [
{ {
"files": ["**/*.astro"], "files": ["**/*.astro"],
"options": { "options": {
"parser": "astro" "parser": "astro"
} }
} }
] ]
} }

View File

@@ -16,8 +16,8 @@ import { rehypeUnicornElementMap } from "./src/utils/markdown/rehype-unicorn-ele
import { rehypeExcerpt } from "./src/utils/markdown/rehype-excerpt"; import { rehypeExcerpt } from "./src/utils/markdown/rehype-excerpt";
import { rehypeUnicornPopulatePost } from "./src/utils/markdown/rehype-unicorn-populate-post"; import { rehypeUnicornPopulatePost } from "./src/utils/markdown/rehype-unicorn-populate-post";
import { rehypeWordCount } from "./src/utils/markdown/rehype-word-count"; import { rehypeWordCount } from "./src/utils/markdown/rehype-word-count";
import {rehypeUnicornGetSuggestedPosts} from "./src/utils/markdown/rehype-unicorn-get-suggested-posts"; import { rehypeUnicornGetSuggestedPosts } from "./src/utils/markdown/rehype-unicorn-get-suggested-posts";
import copy from 'rollup-plugin-copy' import copy from "rollup-plugin-copy";
// TODO: Create types // TODO: Create types
import behead from "remark-behead"; import behead from "remark-behead";
@@ -26,90 +26,93 @@ import rehypeRaw from "rehype-raw";
import image from "@astrojs/image"; import image from "@astrojs/image";
export default defineConfig({ export default defineConfig({
integrations: [image()], integrations: [image()],
vite: { vite: {
ssr: { ssr: {
external: ["svgo"], external: ["svgo"],
}, },
plugins: [ plugins: [
{ {
...copy({ ...copy({
hook: 'options', hook: "options",
flatten: false, flatten: false,
targets: [ targets: [
{ {
src: 'content/**/*', src: "content/**/*",
dest: 'public/content' dest: "public/content",
} },
] ],
}), }),
enforce: 'pre' enforce: "pre",
} },
] ],
}, },
markdown: { markdown: {
mode: "md", mode: "md",
syntaxHighlight: false, syntaxHighlight: false,
extendDefaultPlugins: false, extendDefaultPlugins: false,
remarkPlugins: [ remarkPlugins: [
remarkGfm, remarkGfm,
// Remove complaining about "div cannot be in p element" // Remove complaining about "div cannot be in p element"
remarkUnwrapImages, remarkUnwrapImages,
/* start remark plugins here */ /* start remark plugins here */
[behead, { depth: 1 }], [behead, { depth: 1 }],
[ [
remarkEmbedder as any, remarkEmbedder as any,
{ {
transformers: [oembedTransformer, [TwitchTransformer, { parent }]], transformers: [oembedTransformer, [TwitchTransformer, { parent }]],
} as RemarkEmbedderOptions, } as RemarkEmbedderOptions,
], ],
[ [
remarkTwoslash, remarkTwoslash,
{ {
themes: ["css-variables"], themes: ["css-variables"],
} as UserConfigSettings, } as UserConfigSettings,
], ],
], ],
rehypePlugins: [ rehypePlugins: [
rehypeUnicornPopulatePost, rehypeUnicornPopulatePost,
rehypeUnicornGetSuggestedPosts, rehypeUnicornGetSuggestedPosts,
// This is required to handle unsafe HTML embedded into Markdown // This is required to handle unsafe HTML embedded into Markdown
rehypeRaw, rehypeRaw,
// Do not add the tabs before the slug. We rely on some of the heading // Do not add the tabs before the slug. We rely on some of the heading
// logic in order to do some of the subheading logic // logic in order to do some of the subheading logic
[ [
rehypeSlug, rehypeSlug,
{ {
maintainCase: true, maintainCase: true,
removeAccents: true, removeAccents: true,
enableCustomId: true, enableCustomId: true,
}, },
], ],
[ [
rehypeTabs, rehypeTabs,
{ {
injectSubheaderProps: true, injectSubheaderProps: true,
tabSlugifyProps: { tabSlugifyProps: {
enableCustomId: true, enableCustomId: true,
}, },
}, },
], ],
rehypeHeaderText, rehypeHeaderText,
/** /**
* Insert custom HTML generation code here * Insert custom HTML generation code here
*/ */
[ [
rehypeAstroImageMd, rehypeAstroImageMd,
{ {
maxHeight: 768, maxHeight: 768,
maxWidth: 768, maxWidth: 768,
} },
], ],
rehypeUnicornElementMap, rehypeUnicornElementMap,
[rehypeExcerpt, { [
maxLength: 150 rehypeExcerpt,
}], {
rehypeWordCount maxLength: 150,
], },
} as AstroUserConfig["markdown"] as never, ],
rehypeWordCount,
],
} as AstroUserConfig["markdown"] as never,
}); });

View File

@@ -1,5 +1,5 @@
{ {
"en": "English", "en": "English",
"es": "Español", "es": "Español",
"fr": "Français" "fr": "Français"
} }

View File

@@ -1,42 +1,42 @@
[ [
{ {
"id": "cc-by-nc-sa-4", "id": "cc-by-nc-sa-4",
"footerImg": "https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png", "footerImg": "https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png",
"licenceType": "Creative Commons License", "licenceType": "Creative Commons License",
"explainLink": "http://creativecommons.org/licenses/by-nc-sa/4.0/", "explainLink": "http://creativecommons.org/licenses/by-nc-sa/4.0/",
"name": "Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)", "name": "Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)",
"displayName": "Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License" "displayName": "Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License"
}, },
{ {
"id": "cc-by-4", "id": "cc-by-4",
"footerImg": "https://i.creativecommons.org/l/by/4.0/88x31.png", "footerImg": "https://i.creativecommons.org/l/by/4.0/88x31.png",
"licenceType": "Creative Commons License", "licenceType": "Creative Commons License",
"explainLink": "http://creativecommons.org/licenses/by/4.0/", "explainLink": "http://creativecommons.org/licenses/by/4.0/",
"name": "Attribution 4.0 International (CC BY 4.0)", "name": "Attribution 4.0 International (CC BY 4.0)",
"displayName": "Creative Commons Attribution 4.0 International License" "displayName": "Creative Commons Attribution 4.0 International License"
}, },
{ {
"id": "publicdomain-zero-1", "id": "publicdomain-zero-1",
"footerImg": "https://licensebuttons.net/p/zero/1.0/88x31.png", "footerImg": "https://licensebuttons.net/p/zero/1.0/88x31.png",
"licenceType": "Public Domain", "licenceType": "Public Domain",
"explainLink": "https://creativecommons.org/publicdomain/zero/1.0/", "explainLink": "https://creativecommons.org/publicdomain/zero/1.0/",
"name": "CC0 1.0 Universal (CC0 1.0) Public Domain Dedication", "name": "CC0 1.0 Universal (CC0 1.0) Public Domain Dedication",
"displayName": "Public Domain" "displayName": "Public Domain"
}, },
{ {
"id": "cc-by-nc-nd-4", "id": "cc-by-nc-nd-4",
"footerImg": "https://i.creativecommons.org/l/by-nc-nd/4.0/88x31.png", "footerImg": "https://i.creativecommons.org/l/by-nc-nd/4.0/88x31.png",
"licenceType": "Creative Commons License", "licenceType": "Creative Commons License",
"explainLink": "https://creativecommons.org/licenses/by-nc-nd/4.0/", "explainLink": "https://creativecommons.org/licenses/by-nc-nd/4.0/",
"name": "Attribution-NonCommercial-NoDerivatives 4.0 International (CC BY-NC-ND 4.0)", "name": "Attribution-NonCommercial-NoDerivatives 4.0 International (CC BY-NC-ND 4.0)",
"displayName": "Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International (CC BY-NC-ND 4.0) License" "displayName": "Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International (CC BY-NC-ND 4.0) License"
}, },
{ {
"id": "coderpad", "id": "coderpad",
"footerImg": "/sponsors/coderpad.svg", "footerImg": "/sponsors/coderpad.svg",
"licenceType": "Owned by CoderPad", "licenceType": "Owned by CoderPad",
"explainLink": "https://coderpad.io/about-us/", "explainLink": "https://coderpad.io/about-us/",
"name": "Written for CoderPad, reposted to Unicorn Utterances", "name": "Written for CoderPad, reposted to Unicorn Utterances",
"displayName": "Written for CoderPad" "displayName": "Written for CoderPad"
} }
] ]

View File

@@ -1,322 +1,322 @@
[ [
{ {
"id": "she", "id": "she",
"they": "she", "they": "she",
"them": "her", "them": "her",
"their": "her", "their": "her",
"theirs": "hers", "theirs": "hers",
"themselves": "herself" "themselves": "herself"
}, },
{ {
"id": "he", "id": "he",
"they": "he", "they": "he",
"them": "him", "them": "him",
"their": "his", "their": "his",
"theirs": "his", "theirs": "his",
"themselves": "himself" "themselves": "himself"
}, },
{ {
"id": "they/themselves", "id": "they/themselves",
"they": "they", "they": "they",
"them": "them", "them": "them",
"their": "their", "their": "their",
"theirs": "theirs", "theirs": "theirs",
"themselves": "themselves" "themselves": "themselves"
}, },
{ {
"id": "they/themself", "id": "they/themself",
"they": "they", "they": "they",
"them": "them", "them": "them",
"their": "their", "their": "their",
"theirs": "theirs", "theirs": "theirs",
"themselves": "themself" "themselves": "themself"
}, },
{ {
"id": "ze/hir", "id": "ze/hir",
"they": "ze", "they": "ze",
"them": "hir", "them": "hir",
"their": "hir", "their": "hir",
"theirs": "hirs", "theirs": "hirs",
"themselves": "hirself" "themselves": "hirself"
}, },
{ {
"id": "ze/zir", "id": "ze/zir",
"they": "ze", "they": "ze",
"them": "zir", "them": "zir",
"their": "zir", "their": "zir",
"theirs": "zirs", "theirs": "zirs",
"themselves": "zirself" "themselves": "zirself"
}, },
{ {
"id": "xey", "id": "xey",
"they": "xey", "they": "xey",
"them": "xem", "them": "xem",
"their": "xyr", "their": "xyr",
"theirs": "xyrs", "theirs": "xyrs",
"themselves": "xemself" "themselves": "xemself"
}, },
{ {
"id": "ae", "id": "ae",
"they": "ae", "they": "ae",
"them": "aer", "them": "aer",
"their": "aer", "their": "aer",
"theirs": "aers", "theirs": "aers",
"themselves": "aerself" "themselves": "aerself"
}, },
{ {
"id": "e", "id": "e",
"they": "e", "they": "e",
"them": "em", "them": "em",
"their": "eir", "their": "eir",
"theirs": "eirs", "theirs": "eirs",
"themselves": "emself" "themselves": "emself"
}, },
{ {
"id": "ey", "id": "ey",
"they": "ey", "they": "ey",
"them": "em", "them": "em",
"their": "eir", "their": "eir",
"theirs": "eirs", "theirs": "eirs",
"themselves": "eirself" "themselves": "eirself"
}, },
{ {
"id": "fae", "id": "fae",
"they": "fae", "they": "fae",
"them": "faer", "them": "faer",
"their": "faer", "their": "faer",
"theirs": "faers", "theirs": "faers",
"themselves": "faerself" "themselves": "faerself"
}, },
{ {
"id": "fey", "id": "fey",
"they": "fey", "they": "fey",
"them": "fem", "them": "fem",
"their": "feir", "their": "feir",
"theirs": "feirs", "theirs": "feirs",
"themselves": "feirself" "themselves": "feirself"
}, },
{ {
"id": "hu", "id": "hu",
"they": "hu", "they": "hu",
"them": "hum", "them": "hum",
"their": "hus", "their": "hus",
"theirs": "hus", "theirs": "hus",
"themselves": "humself" "themselves": "humself"
}, },
{ {
"id": "it", "id": "it",
"they": "it", "they": "it",
"them": "it", "them": "it",
"their": "its", "their": "its",
"theirs": "its", "theirs": "its",
"themselves": "itself" "themselves": "itself"
}, },
{ {
"id": "jee", "id": "jee",
"they": "jee", "they": "jee",
"them": "jem", "them": "jem",
"their": "jeir", "their": "jeir",
"theirs": "jeirs", "theirs": "jeirs",
"themselves": "jemself" "themselves": "jemself"
}, },
{ {
"id": "kit", "id": "kit",
"they": "kit", "they": "kit",
"them": "kit", "them": "kit",
"their": "kits", "their": "kits",
"theirs": "kits", "theirs": "kits",
"themselves": "kitself" "themselves": "kitself"
}, },
{ {
"id": "ne", "id": "ne",
"they": "ne", "they": "ne",
"them": "nem", "them": "nem",
"their": "nir", "their": "nir",
"theirs": "nirs", "theirs": "nirs",
"themselves": "nemself" "themselves": "nemself"
}, },
{ {
"id": "peh", "id": "peh",
"they": "peh", "they": "peh",
"them": "pehm", "them": "pehm",
"their": "peh's", "their": "peh's",
"theirs": "peh's", "theirs": "peh's",
"themselves": "pehself" "themselves": "pehself"
}, },
{ {
"id": "per", "id": "per",
"they": "per", "they": "per",
"them": "per", "them": "per",
"their": "per", "their": "per",
"theirs": "pers", "theirs": "pers",
"themselves": "perself" "themselves": "perself"
}, },
{ {
"id": "sie", "id": "sie",
"they": "sie", "they": "sie",
"them": "hir", "them": "hir",
"their": "hir", "their": "hir",
"theirs": "hirs", "theirs": "hirs",
"themselves": "hirself" "themselves": "hirself"
}, },
{ {
"id": "se", "id": "se",
"they": "se", "they": "se",
"them": "sim", "them": "sim",
"their": "ser", "their": "ser",
"theirs": "sers", "theirs": "sers",
"themselves": "serself" "themselves": "serself"
}, },
{ {
"id": "shi", "id": "shi",
"they": "shi", "they": "shi",
"them": "hir", "them": "hir",
"their": "hir", "their": "hir",
"theirs": "hirs", "theirs": "hirs",
"themselves": "hirself" "themselves": "hirself"
}, },
{ {
"id": "si", "id": "si",
"they": "si", "they": "si",
"them": "hyr", "them": "hyr",
"their": "hyr", "their": "hyr",
"theirs": "hyrs", "theirs": "hyrs",
"themselves": "hyrself" "themselves": "hyrself"
}, },
{ {
"id": "they/thonself", "id": "they/thonself",
"they": "thon", "they": "thon",
"them": "thon", "them": "thon",
"their": "thons", "their": "thons",
"theirs": "thons", "theirs": "thons",
"themselves": "thonself" "themselves": "thonself"
}, },
{ {
"id": "ve/vis/verself", "id": "ve/vis/verself",
"they": "ve", "they": "ve",
"them": "ver", "them": "ver",
"their": "vis", "their": "vis",
"theirs": "vis", "theirs": "vis",
"themselves": "verself" "themselves": "verself"
}, },
{ {
"id": "ve/vir/vemself", "id": "ve/vir/vemself",
"they": "ve", "they": "ve",
"them": "vem", "them": "vem",
"their": "vir", "their": "vir",
"theirs": "virs", "theirs": "virs",
"themselves": "vemself" "themselves": "vemself"
}, },
{ {
"id": "vi/ver/verself", "id": "vi/ver/verself",
"they": "vi", "they": "vi",
"them": "ver", "them": "ver",
"their": "ver", "their": "ver",
"theirs": "vers", "theirs": "vers",
"themselves": "verself" "themselves": "verself"
}, },
{ {
"id": "vi/vir/vimself", "id": "vi/vir/vimself",
"they": "vi", "they": "vi",
"them": "vim", "them": "vim",
"their": "vir", "their": "vir",
"theirs": "virs", "theirs": "virs",
"themselves": "vimself" "themselves": "vimself"
}, },
{ {
"id": "vi/vim/vimself", "id": "vi/vim/vimself",
"they": "vi", "they": "vi",
"them": "vim", "them": "vim",
"their": "vim", "their": "vim",
"theirs": "vims", "theirs": "vims",
"themselves": "vimself" "themselves": "vimself"
}, },
{ {
"id": "xie", "id": "xie",
"they": "xie", "they": "xie",
"them": "xer", "them": "xer",
"their": "xer", "their": "xer",
"theirs": "xers", "theirs": "xers",
"themselves": "xerself" "themselves": "xerself"
}, },
{ {
"id": "xe", "id": "xe",
"they": "xe", "they": "xe",
"them": "xem", "them": "xem",
"their": "xyr", "their": "xyr",
"theirs": "xyrs", "theirs": "xyrs",
"themselves": "xemself" "themselves": "xemself"
}, },
{ {
"id": "xey", "id": "xey",
"they": "xey", "they": "xey",
"them": "xem", "them": "xem",
"their": "xeir", "their": "xeir",
"theirs": "xeirs", "theirs": "xeirs",
"themselves": "xemself" "themselves": "xemself"
}, },
{ {
"id": "yo", "id": "yo",
"they": "yo", "they": "yo",
"them": "yo", "them": "yo",
"their": "yos", "their": "yos",
"theirs": "yos", "theirs": "yos",
"themselves": "yosself" "themselves": "yosself"
}, },
{ {
"id": "ze/zirself", "id": "ze/zirself",
"they": "ze", "they": "ze",
"them": "zem", "them": "zem",
"their": "zes", "their": "zes",
"theirs": "zes", "theirs": "zes",
"themselves": "zirself" "themselves": "zirself"
}, },
{ {
"id": "ze", "id": "ze",
"they": "ze", "they": "ze",
"them": "mer", "them": "mer",
"their": "zer", "their": "zer",
"theirs": "zers", "theirs": "zers",
"themselves": "zemself" "themselves": "zemself"
}, },
{ {
"id": "zee", "id": "zee",
"they": "zee", "they": "zee",
"them": "zed", "them": "zed",
"their": "zeta", "their": "zeta",
"theirs": "zetas", "theirs": "zetas",
"themselves": "zedself" "themselves": "zedself"
}, },
{ {
"id": "zie/zir", "id": "zie/zir",
"they": "zie", "they": "zie",
"them": "zir", "them": "zir",
"their": "zir", "their": "zir",
"theirs": "zirs", "theirs": "zirs",
"themselves": "zirself" "themselves": "zirself"
}, },
{ {
"id": "zie/zem", "id": "zie/zem",
"they": "zie", "they": "zie",
"them": "zem", "them": "zem",
"their": "zes", "their": "zes",
"theirs": "zes", "theirs": "zes",
"themselves": "zirself" "themselves": "zirself"
}, },
{ {
"id": "zie/hir", "id": "zie/hir",
"they": "zie", "they": "zie",
"them": "hir", "them": "hir",
"their": "hir", "their": "hir",
"theirs": "hirs", "theirs": "hirs",
"themselves": "hirself" "themselves": "hirself"
}, },
{ {
"id": "zme", "id": "zme",
"they": "zme", "they": "zme",
"them": "zmyr", "them": "zmyr",
"their": "zmyr", "their": "zmyr",
"theirs": "zmyrs", "theirs": "zmyrs",
"themselves": "zmyrself" "themselves": "zmyrself"
} }
] ]

View File

@@ -1,26 +1,26 @@
[ [
{ {
"id": "developer", "id": "developer",
"prettyname": "Developer" "prettyname": "Developer"
}, },
{ {
"id": "designer", "id": "designer",
"prettyname": "Designer" "prettyname": "Designer"
}, },
{ {
"id": "devops", "id": "devops",
"prettyname": "Dev-ops" "prettyname": "Dev-ops"
}, },
{ {
"id": "author", "id": "author",
"prettyname": "Author" "prettyname": "Author"
}, },
{ {
"id": "translator", "id": "translator",
"prettyname": "Translator" "prettyname": "Translator"
}, },
{ {
"id": "community", "id": "community",
"prettyname": "Community Leader" "prettyname": "Community Leader"
} }
] ]

View File

@@ -1,499 +1,427 @@
[ [
{ {
"id": "crutchcorn", "id": "crutchcorn",
"name": "Corbin Crutchley", "name": "Corbin Crutchley",
"firstName": "Corbin", "firstName": "Corbin",
"lastName": "Crutchley", "lastName": "Crutchley",
"description": "Corbin is a senior developer with a passion for helping others. 💜\nThey're focused on ensuring that learning is open and fun. 🦄\nThey blog, livestream, code, and more to reach those goals to help others! 💅", "description": "Corbin is a senior developer with a passion for helping others. 💜\nThey're focused on ensuring that learning is open and fun. 🦄\nThey blog, livestream, code, and more to reach those goals to help others! 💅",
"socials": { "socials": {
"twitter": "crutchcorn", "twitter": "crutchcorn",
"github": "crutchcorn", "github": "crutchcorn",
"twitch": "crutchcorn" "twitch": "crutchcorn"
}, },
"pronouns": "they/themselves", "pronouns": "they/themselves",
"profileImg": "./crutchcorn.png", "profileImg": "./crutchcorn.png",
"color": "#ba68c8", "color": "#ba68c8",
"roles": [ "roles": ["devops", "developer", "author", "community"]
"devops", },
"developer", {
"author", "id": "fennifith",
"community" "name": "James Fenn",
] "firstName": "James",
}, "lastName": "Fenn",
{ "description": "Enjoys writing software on loud keyboards. Starts too many projects. Consumes food.",
"id": "fennifith", "socials": {
"name": "James Fenn", "twitter": "fennifith",
"firstName": "James", "github": "fennifith"
"lastName": "Fenn", },
"description": "Enjoys writing software on loud keyboards. Starts too many projects. Consumes food.", "pronouns": "he",
"socials": { "profileImg": "./fennifith.jpg",
"twitter": "fennifith", "color": "#0091EA",
"github": "fennifith" "roles": ["developer", "author", "community"]
}, },
"pronouns": "he", {
"profileImg": "./fennifith.jpg", "id": "evelynhathaway",
"color": "#0091EA", "name": "Evelyn Hathaway",
"roles": [ "firstName": "Evelyn",
"developer", "lastName": "Hathaway",
"author", "description": "👩‍💻🌈 I'm a student and software developer with a strong passion for frontend and backend JavaScript and web accessibility.",
"community" "socials": {
] "twitter": "eeveedev",
}, "github": "evelynhathaway"
{ },
"id": "evelynhathaway", "pronouns": "she",
"name": "Evelyn Hathaway", "profileImg": "proud.png",
"firstName": "Evelyn", "color": "#ef5f17",
"lastName": "Hathaway", "roles": ["developer", "devops", "community"]
"description": "👩‍💻🌈 I'm a student and software developer with a strong passion for frontend and backend JavaScript and web accessibility.", },
"socials": { {
"twitter": "eeveedev", "id": "adueppen",
"github": "evelynhathaway" "name": "Alex Dueppen",
}, "firstName": "Alex",
"pronouns": "she", "lastName": "Dueppen",
"profileImg": "proud.png", "description": "I do stuff sometimes.",
"color": "#ef5f17", "socials": {
"roles": [ "twitter": "AlexDueppen",
"developer", "github": "adueppen",
"devops", "website": "https://ajd.sh/"
"community" },
] "pronouns": "he",
}, "profileImg": "./adueppen.png",
{ "color": "#69ffff",
"id": "adueppen", "roles": ["developer", "community"]
"name": "Alex Dueppen", },
"firstName": "Alex", {
"lastName": "Dueppen", "id": "zavukodlak",
"description": "I do stuff sometimes.", "name": "Vukašin Anđelković",
"socials": { "firstName": "Vukašin",
"twitter": "AlexDueppen", "lastName": "Anđelković",
"github": "adueppen", "description": "Always doing a little bit of everything. Although, mainly focusing on design, logos, illustrations and paintings.\nI created the logo and all of the unicorns you see peeking out in some of the profile pictures!",
"website": "https://ajd.sh/" "socials": {
}, "twitter": "vukkashin",
"pronouns": "he", "website": "https://vukash.in/",
"profileImg": "./adueppen.png", "dribbble": "vukashin"
"color": "#69ffff", },
"roles": [ "pronouns": "he",
"developer", "profileImg": "./vukashin.png",
"community" "color": "#3485FF",
] "roles": ["designer"]
}, },
{ {
"id": "zavukodlak", "id": "tommyemo",
"name": "Vukašin Anđelković", "name": "Tom Wellington",
"firstName": "Vukašin", "firstName": "Tom",
"lastName": "Anđelković", "lastName": "Wellington",
"description": "Always doing a little bit of everything. Although, mainly focusing on design, logos, illustrations and paintings.\nI created the logo and all of the unicorns you see peeking out in some of the profile pictures!", "description": "I design icons and user interfaces, among other things. he/him ✌️",
"socials": { "socials": {
"twitter": "vukkashin", "twitter": "tommy_emo_",
"website": "https://vukash.in/", "website": "https://www.tommyemo.net/"
"dribbble": "vukashin" },
}, "pronouns": "he",
"pronouns": "he", "profileImg": "./tommyemo.jpg",
"profileImg": "./vukashin.png", "color": "#8539EB",
"color": "#3485FF", "roles": ["designer"]
"roles": [ },
"designer" {
] "id": "edpratti",
}, "name": "Eduardo Pratti",
{ "firstName": "Eduardo",
"id": "tommyemo", "lastName": "Pratti",
"name": "Tom Wellington", "description": "UI designer and developer wannabe. Cares about negative space, layout grids and Bloodborne challenge runs.",
"firstName": "Tom", "socials": {
"lastName": "Wellington", "twitter": "edpratti",
"description": "I design icons and user interfaces, among other things. he/him ✌️", "website": "http://pratti.design"
"socials": { },
"twitter": "tommy_emo_", "pronouns": "he",
"website": "https://www.tommyemo.net/" "profileImg": "./edpratti.jpg",
}, "color": "#FF3300",
"pronouns": "he", "roles": ["designer", "author"]
"profileImg": "./tommyemo.jpg", },
"color": "#8539EB", {
"roles": [ "id": "sarsamurmu",
"designer" "name": "Sarsa Murmu",
] "firstName": "Sarsa",
}, "lastName": "Murmu",
{ "description": "A High School Web Dev. Likes Android Development too. On the way to be an expert in Front-end. Checkout GitHub for more.",
"id": "edpratti", "socials": {
"name": "Eduardo Pratti", "twitter": "sarsamurmu",
"firstName": "Eduardo", "github": "sarsamurmu",
"lastName": "Pratti", "website": "https://sarsamurmu.github.io"
"description": "UI designer and developer wannabe. Cares about negative space, layout grids and Bloodborne challenge runs.", },
"socials": { "pronouns": "he",
"twitter": "edpratti", "profileImg": "./sarsamurmu.png",
"website": "http://pratti.design" "color": "#7C4DFF",
}, "roles": ["developer"]
"pronouns": "he", },
"profileImg": "./edpratti.jpg", {
"color": "#FF3300", "id": "MDutro",
"roles": [ "name": "Micah Dutro",
"designer", "firstName": "Micah",
"author" "lastName": "Dutro",
] "description": "A non-profit lawyer turned budding web developer.",
}, "socials": {
{ "github": "MDutro"
"id": "sarsamurmu", },
"name": "Sarsa Murmu", "pronouns": "he",
"firstName": "Sarsa", "profileImg": "./mdutro.jpg",
"lastName": "Murmu", "color": "#7C4DFF",
"description": "A High School Web Dev. Likes Android Development too. On the way to be an expert in Front-end. Checkout GitHub for more.", "roles": ["developer", "author", "community"]
"socials": { },
"twitter": "sarsamurmu", {
"github": "sarsamurmu", "id": "reikaze",
"website": "https://sarsamurmu.github.io" "name": "Kevin Mai",
}, "firstName": "Kevin",
"pronouns": "he", "lastName": "Mai",
"profileImg": "./sarsamurmu.png", "description": "Hello! I'm Kevin Phong Mai, aka Reikaze or RockmanDash12, a Computer Engineering Student and Freelance Writer passionate about Tech, Anime, Visual Novels and much more. I'm the Owner of RockmanDash Reviews Blog, and I write for the AniTAY & FuwaNovel blogs.",
"color": "#7C4DFF", "socials": {
"roles": [ "twitter": "Reikaze0",
"developer" "github": "Reikaze"
] },
}, "pronouns": "he",
{ "profileImg": "./reikaze.jpg",
"id": "MDutro", "color": "#ba68c8",
"name": "Micah Dutro", "roles": ["author"]
"firstName": "Micah", },
"lastName": "Dutro", {
"description": "A non-profit lawyer turned budding web developer.", "id": "thodges314",
"socials": { "name": "Thomas Hodges",
"github": "MDutro" "firstName": "Thomas",
}, "lastName": "Hodges",
"pronouns": "he", "description": "A software engineer with a mathematical background, professional experience in frontend, primarily with Reactjs and D3js, and a strong interest in mathematical modeling and visualisations.",
"profileImg": "./mdutro.jpg", "socials": {
"color": "#7C4DFF", "github": "thodges314",
"roles": [ "linkedIn": "thomas-hodges"
"developer", },
"author", "pronouns": "he",
"community" "profileImg": "./thodges.png",
] "color": "#ba68c8",
}, "roles": ["author"]
{ },
"id": "reikaze", {
"name": "Kevin Mai", "id": "skatcat31",
"firstName": "Kevin", "name": "Robert Mennell",
"lastName": "Mai", "firstName": "Robert",
"description": "Hello! I'm Kevin Phong Mai, aka Reikaze or RockmanDash12, a Computer Engineering Student and Freelance Writer passionate about Tech, Anime, Visual Novels and much more. I'm the Owner of RockmanDash Reviews Blog, and I write for the AniTAY & FuwaNovel blogs.", "lastName": "Mennell",
"socials": { "description": "A fullstack engineer who loves learning new things, playing video games, and his wife.\nIf you can learn it, you can do it.\nIf you can do it well, you've learned it.",
"twitter": "Reikaze0", "socials": {
"github": "Reikaze" "github": "skatcat31",
}, "linkedIn": "rnmennell"
"pronouns": "he", },
"profileImg": "./reikaze.jpg", "color": "#ba68c8",
"color": "#ba68c8", "profileImg": "./hello.png",
"roles": [ "pronouns": "he",
"author" "roles": ["author", "community"]
] },
}, {
{ "id": "seanmiller",
"id": "thodges314", "name": "Sean Miller",
"name": "Thomas Hodges", "firstName": "Sean",
"firstName": "Thomas", "lastName": "Miller",
"lastName": "Hodges", "description": "Howdy! Computer Science major at Texas A&M University, with a minor in cybersecurity. Super passionate about all things software!",
"description": "A software engineer with a mathematical background, professional experience in frontend, primarily with Reactjs and D3js, and a strong interest in mathematical modeling and visualisations.", "socials": {
"socials": { "twitter": "beastosean",
"github": "thodges314", "github": "tamuseanmiller",
"linkedIn": "thomas-hodges" "website": "https://sean.millerfamily.tech",
}, "linkedIn": "tamuseanmiller"
"pronouns": "he", },
"profileImg": "./thodges.png", "pronouns": "he",
"color": "#ba68c8", "profileImg": "./seanmiller.jpg",
"roles": [ "color": "#551a8b",
"author" "roles": ["author"]
] },
}, {
{ "id": "pierremtb",
"id": "skatcat31", "name": "Pierre Jacquier",
"name": "Robert Mennell", "firstName": "Pierre",
"firstName": "Robert", "lastName": "Jacquier",
"lastName": "Mennell", "description": "Junior Hardware Engineer at Algolux. Computationally curious.",
"description": "A fullstack engineer who loves learning new things, playing video games, and his wife.\nIf you can learn it, you can do it.\nIf you can do it well, you've learned it.", "socials": {
"socials": { "twitter": "PierreJacquier",
"github": "skatcat31", "github": "pierremtb",
"linkedIn": "rnmennell" "website": "https://pierrejacquier.com",
}, "linkedIn": "pierrejacquier"
"color": "#ba68c8", },
"profileImg": "./hello.png", "pronouns": "he",
"pronouns": "he", "profileImg": "./pierremtb.jpg",
"roles": [ "color": "#FFEB3B",
"author", "roles": ["author"]
"community" },
] {
}, "id": "maisydino",
{ "name": "Maisy Dinosaur",
"id": "seanmiller", "firstName": "Maisy",
"name": "Sean Miller", "lastName": "Dinosaur",
"firstName": "Sean", "description": "I do a lot of stuff sometimes. Part-time fullstack, full time dog petter.",
"lastName": "Miller", "socials": {
"description": "Howdy! Computer Science major at Texas A&M University, with a minor in cybersecurity. Super passionate about all things software!", "twitter": "rodentman87",
"socials": { "github": "rodentman87",
"twitter": "beastosean", "website": "https://likesdinosaurs.com"
"github": "tamuseanmiller", },
"website": "https://sean.millerfamily.tech", "pronouns": "she",
"linkedIn": "tamuseanmiller" "profileImg": "./maisydino.jpg",
}, "color": "#FDF6E3",
"pronouns": "he", "roles": ["author", "community"]
"profileImg": "./seanmiller.jpg", },
"color": "#551a8b", {
"roles": [ "id": "bobrossrtx",
"author" "name": "Bobrossrtx",
] "firstName": "Owen",
}, "lastName": "Boreham",
{ "description": "I have over 1000 years of software development experience, do not underestimate me!",
"id": "pierremtb", "socials": {
"name": "Pierre Jacquier", "twitter": "bobrossrtx",
"firstName": "Pierre", "github": "bobrossrtx",
"lastName": "Jacquier", "website": "https://www.owenboreham.tech"
"description": "Junior Hardware Engineer at Algolux. Computationally curious.", },
"socials": { "pronouns": "he",
"twitter": "PierreJacquier", "profileImg": "./bobrossrtx.jpg",
"github": "pierremtb", "color": "#b7e11e",
"website": "https://pierrejacquier.com", "roles": ["developer", "author"]
"linkedIn": "pierrejacquier" },
}, {
"pronouns": "he", "id": "ljtech",
"profileImg": "./pierremtb.jpg", "name": "Landon Johnson",
"color": "#FFEB3B", "firstName": "Landon",
"roles": [ "lastName": "Johnson",
"author" "description": "Hello there, my name is Lj. I am a full stack developer.",
] "socials": {
}, "twitter": "ljtechdotca",
{ "github": "ljtechdotca",
"id": "maisydino", "twitch": "ljtechdotca",
"name": "Maisy Dinosaur", "website": "https://ljtech.ca"
"firstName": "Maisy", },
"lastName": "Dinosaur", "pronouns": "he",
"description": "I do a lot of stuff sometimes. Part-time fullstack, full time dog petter.", "profileImg": "./ljtechdotca.png",
"socials": { "color": "#7b61ff",
"twitter": "rodentman87", "roles": ["author"]
"github": "rodentman87", },
"website": "https://likesdinosaurs.com" {
}, "id": "SkyHawk_0",
"pronouns": "she", "name": "Joshua Hawkins",
"profileImg": "./maisydino.jpg", "firstName": "Joshua",
"color": "#FDF6E3", "lastName": "Hawkins",
"roles": [ "description": "I am a high school student who focuses on python. Most of my scripts I have made where just for fun or to work something out that I couldn't.",
"author", "socials": {},
"community" "pronouns": "he",
] "profileImg": "./goofy.png",
}, "color": "#18BBC9",
{ "roles": ["author"]
"id": "bobrossrtx", },
"name": "Bobrossrtx", {
"firstName": "Owen", "id": "splatkillwill",
"lastName": "Boreham", "name": "William (Will) Lohan",
"description": "I have over 1000 years of software development experience, do not underestimate me!", "firstName": "William",
"socials": { "lastName": "Lohan",
"twitter": "bobrossrtx", "description": "",
"github": "bobrossrtx", "socials": {
"website": "https://www.owenboreham.tech" "github": "william-lohan",
}, "twitch": "splat_killwill",
"pronouns": "he", "website": "https://gatimus.com/",
"profileImg": "./bobrossrtx.jpg", "linkedIn": "william-lohan-b202637a"
"color": "#b7e11e", },
"roles": [ "pronouns": "they/themselves",
"developer", "profileImg": "./splatkillwill.jpg",
"author" "color": "#BF00FF",
] "roles": ["author"]
}, },
{ {
"id": "ljtech", "id": "fmothe",
"name": "Landon Johnson", "name": "Federico Mothe",
"firstName": "Landon", "firstName": "Federico",
"lastName": "Johnson", "lastName": "Mothe",
"description": "Hello there, my name is Lj. I am a full stack developer.", "description": "Software Dev from Argentina learning the dark magic of frontend development and typescript.",
"socials": { "socials": {
"twitter": "ljtechdotca", "twitter": "FedericoMothe",
"github": "ljtechdotca", "github": "fmothe",
"twitch": "ljtechdotca", "twitch": "mothevv"
"website": "https://ljtech.ca" },
}, "pronouns": "he",
"pronouns": "he", "profileImg": "./fmothe.jpg",
"profileImg": "./ljtechdotca.png", "color": "#18BBC9",
"color": "#7b61ff", "roles": ["translator"]
"roles": [ },
"author" {
] "id": "jahirfiquitiva",
}, "name": "Jahir Fiquitiva",
{ "firstName": "Jahir",
"id": "SkyHawk_0", "lastName": "Fiquitiva",
"name": "Joshua Hawkins", "description": "Passionate and creative full-stack software engineer based in Colombia 🇨🇴.",
"firstName": "Joshua", "socials": {
"lastName": "Hawkins", "twitter": "jahirfiquitiva",
"description": "I am a high school student who focuses on python. Most of my scripts I have made where just for fun or to work something out that I couldn't.", "github": "jahirfiquitiva",
"socials": {}, "website": "https://jahir.dev",
"pronouns": "he", "linkedIn": "jahirfiquitiva"
"profileImg": "./goofy.png", },
"color": "#18BBC9", "pronouns": "he",
"roles": [ "profileImg": "./jahirfiquitiva.jpg",
"author" "color": "#3867d6",
] "roles": ["translator"]
}, },
{ {
"id": "splatkillwill", "id": "kaleem",
"name": "William (Will) Lohan", "name": "Kaleem",
"firstName": "William", "firstName": "Kaleem",
"lastName": "Lohan", "lastName": "",
"description": "", "description": "Software Engineer, Simplifying programming, writing about learnings and lessons learned.",
"socials": { "socials": {
"github": "william-lohan", "twitter": "kaleemniz",
"twitch": "splat_killwill", "github": "kaleem68",
"website": "https://gatimus.com/", "linkedIn": "nixamani5"
"linkedIn": "william-lohan-b202637a" },
}, "pronouns": "he",
"pronouns": "they/themselves", "profileImg": "./kaleem.jpeg",
"profileImg": "./splatkillwill.jpg", "color": "#a8b3ba",
"color": "#BF00FF", "roles": ["author"]
"roles": [ },
"author" {
] "id": "qarnax",
}, "name": "Qarnax",
{ "firstName": "",
"id": "fmothe", "lastName": "",
"name": "Federico Mothe", "description": "I'm a frontend developer and indie game enthusiast 👾 \n I enjoy learning new things and building my own stuff 🔧 and I love helping people get into coding 😊",
"firstName": "Federico", "socials": {
"lastName": "Mothe", "twitch": "qarnax_",
"description": "Software Dev from Argentina learning the dark magic of frontend development and typescript.", "twitter": "qarnax",
"socials": { "github": "qarnax801"
"twitter": "FedericoMothe", },
"github": "fmothe", "profileImg": "./qarnax.jpg",
"twitch": "mothevv" "color": "",
}, "roles": ["developer", "author", "community", "translator"]
"pronouns": "he", },
"profileImg": "./fmothe.jpg", {
"color": "#18BBC9", "id": "alexchadwick",
"roles": [ "name": "Alex Chadwick",
"translator" "firstName": "Alex",
] "lastName": "Chadwick",
}, "description": "I'm a full-stack web developer in the UK (but born in sunny Spain!) \n I spend too much time reading articles on clean code and not enough refactoring 🤣",
{ "socials": {
"id": "jahirfiquitiva", "twitch": "alexchadwicc",
"name": "Jahir Fiquitiva", "twitter": "TheAlexChadwick",
"firstName": "Jahir", "github": "AlexChadwickP",
"lastName": "Fiquitiva", "linkedin": "alexchadwickp",
"description": "Passionate and creative full-stack software engineer based in Colombia 🇨🇴.", "website": "https://alexchadwick.com"
"socials": { },
"twitter": "jahirfiquitiva", "pronouns": "he",
"github": "jahirfiquitiva", "profileImg": "./alexchadwick.jpg",
"website": "https://jahir.dev", "color": "",
"linkedIn": "jahirfiquitiva" "roles": ["author", "translator"]
}, },
"pronouns": "he", {
"profileImg": "./jahirfiquitiva.jpg", "id": "williamcook",
"color": "#3867d6", "name": "William George Cook",
"roles": [ "firstName": "William",
"translator" "lastName": "Cook",
] "description": "Full stack developer who loves to help! Find me in the garden, up a tree, or at Disney World ✨",
}, "socials": {
{ "twitter": "wgeorgecook",
"id": "kaleem", "github": "wgeorgecook",
"name": "Kaleem", "linkedin": "wgeorgecook",
"firstName": "Kaleem", "website": "https://williamgeorgecook.com"
"lastName": "", },
"description": "Software Engineer, Simplifying programming, writing about learnings and lessons learned.", "pronouns": "he",
"socials": { "profileImg": "./williamcook.jpg",
"twitter": "kaleemniz", "color": "#AF7AC5",
"github": "kaleem68", "roles": ["author"]
"linkedIn": "nixamani5" },
}, {
"pronouns": "he", "id": "rudy",
"profileImg": "./kaleem.jpeg", "name": "Rudolph Schmitz",
"color": "#a8b3ba", "firstName": "Rudolph",
"roles": [ "lastName": "Schmitz",
"author" "description": "Full stack dev with frontend emphasis. Enjoys all things Typescript, soccer, and my mom is the best.",
] "socials": {
}, "twitter": "rudolphschmitz",
{ "github": "rwschmitz"
"id": "qarnax", },
"name": "Qarnax", "pronouns": "he",
"firstName": "", "profileImg": "./rudy.jpg",
"lastName": "", "color": "#ba68c8",
"description": "I'm a frontend developer and indie game enthusiast 👾 \n I enjoy learning new things and building my own stuff 🔧 and I love helping people get into coding 😊", "roles": ["community"]
"socials": { },
"twitch": "qarnax_", {
"twitter": "qarnax", "id": "LayZee",
"github": "qarnax801" "name": "Lars Gyrup Brink Nielsen",
}, "firstName": "Lars",
"profileImg": "./qarnax.jpg", "lastName": "Gyrup Brink Nielsen",
"color": "", "description": "Hi there, I'm Lars 👋\n\nI am a public tech contributor. I write articles and books, I organize communities, and I maintain open source software.",
"roles": [ "socials": {
"developer", "twitter": "LayZeeDK",
"author", "github": "LayZeeDK",
"community", "twitch": "LayZeeDK"
"translator" },
] "pronouns": "he",
}, "profileImg": "./lars-gyrup-brink-nielsen.jpg",
{ "color": "#1b9bf0",
"id": "alexchadwick", "roles": ["author"]
"name": "Alex Chadwick", }
"firstName": "Alex",
"lastName": "Chadwick",
"description": "I'm a full-stack web developer in the UK (but born in sunny Spain!) \n I spend too much time reading articles on clean code and not enough refactoring 🤣",
"socials": {
"twitch": "alexchadwicc",
"twitter": "TheAlexChadwick",
"github": "AlexChadwickP",
"linkedin": "alexchadwickp",
"website": "https://alexchadwick.com"
},
"pronouns": "he",
"profileImg": "./alexchadwick.jpg",
"color": "",
"roles": [
"author",
"translator"
]
},
{
"id": "williamcook",
"name": "William George Cook",
"firstName": "William",
"lastName": "Cook",
"description": "Full stack developer who loves to help! Find me in the garden, up a tree, or at Disney World ✨",
"socials": {
"twitter": "wgeorgecook",
"github": "wgeorgecook",
"linkedin": "wgeorgecook",
"website": "https://williamgeorgecook.com"
},
"pronouns": "he",
"profileImg": "./williamcook.jpg",
"color": "#AF7AC5",
"roles": [
"author"
]
},
{
"id": "rudy",
"name": "Rudolph Schmitz",
"firstName": "Rudolph",
"lastName": "Schmitz",
"description": "Full stack dev with frontend emphasis. Enjoys all things Typescript, soccer, and my mom is the best.",
"socials": {
"twitter": "rudolphschmitz",
"github": "rwschmitz"
},
"pronouns": "he",
"profileImg": "./rudy.jpg",
"color": "#ba68c8",
"roles": [
"community"
]
},
{
"id": "LayZee",
"name": "Lars Gyrup Brink Nielsen",
"firstName": "Lars",
"lastName": "Gyrup Brink Nielsen",
"description": "Hi there, I'm Lars 👋\n\nI am a public tech contributor. I write articles and books, I organize communities, and I maintain open source software.",
"socials": {
"twitter": "LayZeeDK",
"github": "LayZeeDK",
"twitch": "LayZeeDK"
},
"pronouns": "he",
"profileImg": "./lars-gyrup-brink-nielsen.jpg",
"color": "#1b9bf0",
"roles": [
"author"
]
}
] ]

View File

@@ -1,71 +1,71 @@
{ {
"name": "unicorn-utterances-site", "name": "unicorn-utterances-site",
"private": true, "private": true,
"description": "Learning programming from magically majestic words", "description": "Learning programming from magically majestic words",
"version": "0.3.0-alpha.1", "version": "0.3.0-alpha.1",
"bugs": { "bugs": {
"url": "https://github.com/unicorn-utterances/unicorn-utterances/issues" "url": "https://github.com/unicorn-utterances/unicorn-utterances/issues"
}, },
"homepage": "https://unicorn-utterances.com", "homepage": "https://unicorn-utterances.com",
"keywords": [ "keywords": [
"blog", "blog",
"education", "education",
"programming" "programming"
], ],
"license": "MPL-2.0", "license": "MPL-2.0",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/unicorn-utterances/unicorn-utterances.git" "url": "git+https://github.com/unicorn-utterances/unicorn-utterances.git"
}, },
"scripts": { "scripts": {
"debug": "node --inspect-brk ./node_modules/astro/astro.js dev --experimental-integrations", "debug": "node --inspect-brk ./node_modules/astro/astro.js dev --experimental-integrations",
"dev": "astro dev --experimental-integrations", "dev": "astro dev --experimental-integrations",
"start": "astro dev --experimental-integrations", "start": "astro dev --experimental-integrations",
"build": "astro build --experimental-integrations", "build": "astro build --experimental-integrations",
"preview": "astro preview --experimental-integrations", "preview": "astro preview --experimental-integrations",
"host:local": "cd dist && ws --http2 --compress", "host:local": "cd dist && ws --http2 --compress",
"format": "prettier -w . --cache --plugin-search-dir=.", "format": "prettier -w . --cache --plugin-search-dir=.",
"lint": "eslint . --ext .js,.ts,.astro" "lint": "eslint . --ext .js,.ts,.astro"
}, },
"devDependencies": { "devDependencies": {
"@astrojs/image": "^0.7.1", "@astrojs/image": "^0.7.1",
"@remark-embedder/core": "^3.0.1", "@remark-embedder/core": "^3.0.1",
"@remark-embedder/transformer-oembed": "^3.0.0", "@remark-embedder/transformer-oembed": "^3.0.0",
"@types/classnames": "^2.3.1", "@types/classnames": "^2.3.1",
"@types/node": "^18.7.18", "@types/node": "^18.7.18",
"@typescript-eslint/parser": "^4.33.0", "@typescript-eslint/parser": "^4.33.0",
"astro": "^1.2.8", "astro": "^1.2.8",
"astro-icon": "^0.7.3", "astro-icon": "^0.7.3",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"dayjs": "^1.11.5", "dayjs": "^1.11.5",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"eslint-plugin-astro": "^0.19.0", "eslint-plugin-astro": "^0.19.0",
"eslint-plugin-jsx-a11y": "^6.6.1", "eslint-plugin-jsx-a11y": "^6.6.1",
"gatsby-remark-embedder": "^6.0.1", "gatsby-remark-embedder": "^6.0.1",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"hast": "^1.0.0", "hast": "^1.0.0",
"hast-util-from-html": "^1.0.0", "hast-util-from-html": "^1.0.0",
"hast-util-has-property": "^2.0.0", "hast-util-has-property": "^2.0.0",
"hast-util-heading-rank": "^2.1.0", "hast-util-heading-rank": "^2.1.0",
"hast-util-to-string": "^2.0.0", "hast-util-to-string": "^2.0.0",
"image-size": "^1.0.2", "image-size": "^1.0.2",
"junk": "^4.0.0", "junk": "^4.0.0",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"prettier-plugin-astro": "^0.5.4", "prettier-plugin-astro": "^0.5.4",
"rehype-raw": "^6.1.1", "rehype-raw": "^6.1.1",
"rehype-retext": "^3.0.2", "rehype-retext": "^3.0.2",
"rehype-slug-custom-id": "^1.1.0", "rehype-slug-custom-id": "^1.1.0",
"remark-behead": "^3.1.0", "remark-behead": "^3.1.0",
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
"remark-shiki-twoslash": "^3.1.0", "remark-shiki-twoslash": "^3.1.0",
"remark-unwrap-images": "^3.0.1", "remark-unwrap-images": "^3.0.1",
"retext-english": "^4.1.0", "retext-english": "^4.1.0",
"rollup-plugin-copy": "^3.4.0", "rollup-plugin-copy": "^3.4.0",
"sass": "^1.54.9", "sass": "^1.54.9",
"slash": "^4.0.0", "slash": "^4.0.0",
"terser": "^5.15.0", "terser": "^5.15.0",
"unified": "^10.1.2", "unified": "^10.1.2",
"unist-util-replace-all-between": "^0.1.1", "unist-util-replace-all-between": "^0.1.1",
"unist-util-visit": "^4.1.1" "unist-util-visit": "^4.1.1"
} }
} }

View File

@@ -1,51 +1,51 @@
{ {
"name": "Unicorn Utterances", "name": "Unicorn Utterances",
"short_name": "Unicorn Utterances", "short_name": "Unicorn Utterances",
"start_url": "/", "start_url": "/",
"background_color": "#ffffff", "background_color": "#ffffff",
"theme_color": "#127db3", "theme_color": "#127db3",
"display": "minimal-ui", "display": "minimal-ui",
"cacheDigest": "2a", "cacheDigest": "2a",
"icons": [ "icons": [
{ {
"src": "icons/icon-48x48.png?v=2a", "src": "icons/icon-48x48.png?v=2a",
"sizes": "48x48", "sizes": "48x48",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "icons/icon-72x72.png?v=2a", "src": "icons/icon-72x72.png?v=2a",
"sizes": "72x72", "sizes": "72x72",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "icons/icon-96x96.png?v=2a", "src": "icons/icon-96x96.png?v=2a",
"sizes": "96x96", "sizes": "96x96",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "icons/icon-144x144.png?v=2a", "src": "icons/icon-144x144.png?v=2a",
"sizes": "144x144", "sizes": "144x144",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "icons/icon-192x192.png?v=2a", "src": "icons/icon-192x192.png?v=2a",
"sizes": "192x192", "sizes": "192x192",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "icons/icon-256x256.png?v=2a", "src": "icons/icon-256x256.png?v=2a",
"sizes": "256x256", "sizes": "256x256",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "icons/icon-384x384.png?v=2a", "src": "icons/icon-384x384.png?v=2a",
"sizes": "384x384", "sizes": "384x384",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "icons/icon-512x512.png?v=2a", "src": "icons/icon-512x512.png?v=2a",
"sizes": "512x512", "sizes": "512x512",
"type": "image/png" "type": "image/png"
} }
] ]
} }

View File

@@ -1,26 +1,26 @@
/* AFTER CHANGING THIS FILE, PLEASE MANUALLY MINIFY IT AND PUT INTO backbtn.min.js */ /* AFTER CHANGING THIS FILE, PLEASE MANUALLY MINIFY IT AND PUT INTO backbtn.min.js */
/* TODO: Add minifier to build script */ /* TODO: Add minifier to build script */
window.onload = () => { window.onload = () => {
const backBtn = document.querySelector('#backbtn'); const backBtn = document.querySelector("#backbtn");
let hasHistory = false; let hasHistory = false;
window.addEventListener('beforeunload', () => { window.addEventListener("beforeunload", () => {
hasHistory = true; hasHistory = true;
}) });
backBtn.addEventListener('click', () => { backBtn.addEventListener("click", () => {
if (!document.referrer) { if (!document.referrer) {
// This is the first page the user has visited on the site in this session // This is the first page the user has visited on the site in this session
window.location.href = '/'; window.location.href = "/";
return; return;
} }
history.back(); history.back();
// User cannot go back, meaning that we're at the first page of the site session // User cannot go back, meaning that we're at the first page of the site session
setTimeout(() => { setTimeout(() => {
if (!hasHistory){ if (!hasHistory) {
window.location.href = "/"; window.location.href = "/";
} }
}, 200); }, 200);
}) });
} };

View File

@@ -2,166 +2,169 @@
const LOCAL_STORAGE_KEY = "tabs-selection"; const LOCAL_STORAGE_KEY = "tabs-selection";
window.addEventListener('DOMContentLoaded', () => { window.addEventListener("DOMContentLoaded", () => {
const tabLists = document.querySelectorAll('[role="tablist"]'); const tabLists = document.querySelectorAll('[role="tablist"]');
tabLists.forEach(tabList => { tabLists.forEach((tabList) => {
/** /**
* @type {NodeListOf<HTMLElement>} * @type {NodeListOf<HTMLElement>}
*/ */
const tabs = tabList.querySelectorAll('[role="tab"]'); const tabs = tabList.querySelectorAll('[role="tab"]');
// Add a click event handler to each tab // Add a click event handler to each tab
tabs.forEach((tab) => { tabs.forEach((tab) => {
tab.addEventListener('click', e => { tab.addEventListener("click", (e) => {
/** /**
* @type {HTMLElement} * @type {HTMLElement}
*/ */
const target = e.target; const target = e.target;
// Scroll onto screen in order to avoid jumping page locations // Scroll onto screen in order to avoid jumping page locations
setTimeout(() => { setTimeout(() => {
target.scrollIntoView({ target.scrollIntoView({
behavior: "auto", behavior: "auto",
block: "center", block: "center",
inline: "center", inline: "center",
}); });
}, 0); }, 0);
changeTabs({ target }) changeTabs({ target });
}); });
}); });
// Enable arrow navigation between tabs in the tab list // Enable arrow navigation between tabs in the tab list
let tabFocus = 0; let tabFocus = 0;
tabList.addEventListener('keydown', (_e) => { tabList.addEventListener("keydown", (_e) => {
/** /**
* @type {KeyboardEvent} * @type {KeyboardEvent}
*/ */
const e = _e; const e = _e;
// Move right // Move right
if (e.keyCode === 39 || e.keyCode === 37) { if (e.keyCode === 39 || e.keyCode === 37) {
tabs[tabFocus].setAttribute('tabindex', `-1`); tabs[tabFocus].setAttribute("tabindex", `-1`);
if (e.keyCode === 39) { if (e.keyCode === 39) {
tabFocus++; tabFocus++;
// If we're at the end, go to the start // If we're at the end, go to the start
if (tabFocus >= tabs.length) { if (tabFocus >= tabs.length) {
tabFocus = 0; tabFocus = 0;
} }
// Move left // Move left
} else if (e.keyCode === 37) { } else if (e.keyCode === 37) {
tabFocus--; tabFocus--;
// If we're at the start, move to the end // If we're at the start, move to the end
if (tabFocus < 0) { if (tabFocus < 0) {
tabFocus = tabs.length - 1; tabFocus = tabs.length - 1;
} }
} }
tabs[tabFocus].setAttribute('tabindex', `0`); tabs[tabFocus].setAttribute("tabindex", `0`);
tabs[tabFocus].focus(); tabs[tabFocus].focus();
tabs[tabFocus].click(); tabs[tabFocus].click();
} }
}); });
}); });
const currentTab = localStorage.getItem(LOCAL_STORAGE_KEY); const currentTab = localStorage.getItem(LOCAL_STORAGE_KEY);
if (currentTab) { if (currentTab) {
/** /**
* @type {HTMLElement} * @type {HTMLElement}
*/ */
const el = document.querySelector(`[data-tabname="${currentTab}"]`); const el = document.querySelector(`[data-tabname="${currentTab}"]`);
if (el) changeTabs({ target: el }); if (el) changeTabs({ target: el });
} }
function changeTabs(_e) { function changeTabs(_e) {
/** /**
* @type {{ target: HTMLElement }} * @type {{ target: HTMLElement }}
*/ */
const e = _e; const e = _e;
const target = e.target; const target = e.target;
const parent = target.parentNode; const parent = target.parentNode;
const grandparent = parent.parentNode; const grandparent = parent.parentNode;
// Remove all current selected tabs // Remove all current selected tabs
parent parent
.querySelectorAll('[aria-selected="true"]') .querySelectorAll('[aria-selected="true"]')
.forEach((t) => t.setAttribute('aria-selected', `false`)); .forEach((t) => t.setAttribute("aria-selected", `false`));
// Set this tab as selected // Set this tab as selected
target.setAttribute('aria-selected', `true`); target.setAttribute("aria-selected", `true`);
const tabName = target.dataset.tabname; const tabName = target.dataset.tabname;
/** /**
* @type {NodeListOf<HTMLElement>} * @type {NodeListOf<HTMLElement>}
*/ */
const relatedTabs = document.querySelectorAll(`[role="tab"][data-tabname="${target.dataset.tabname}"]`); const relatedTabs = document.querySelectorAll(
`[role="tab"][data-tabname="${target.dataset.tabname}"]`
);
localStorage.setItem(LOCAL_STORAGE_KEY, tabName); localStorage.setItem(LOCAL_STORAGE_KEY, tabName);
for (let relatedTab of relatedTabs) { for (let relatedTab of relatedTabs) {
if (relatedTab === target) continue; if (relatedTab === target) continue;
changeTabs({ target: relatedTab }); changeTabs({ target: relatedTab });
} }
// Hide all tab panels // Hide all tab panels
grandparent grandparent
.querySelectorAll('[role="tabpanel"]') .querySelectorAll('[role="tabpanel"]')
.forEach((p) => p.setAttribute('hidden', `true`)); .forEach((p) => p.setAttribute("hidden", `true`));
// Show the selected panel // Show the selected panel
grandparent.parentNode grandparent.parentNode
.querySelector(`#${target.getAttribute('aria-controls')}`) .querySelector(`#${target.getAttribute("aria-controls")}`)
.removeAttribute('hidden'); .removeAttribute("hidden");
} }
/* -------------------- */ /* -------------------- */
/** /**
* *
* @param {HTMLElement} el * @param {HTMLElement} el
* @param {(el: HTMLElement) => boolean} check * @param {(el: HTMLElement) => boolean} check
* @returns {boolean} * @returns {boolean}
*/ */
function checkElementsParents(el, check) { function checkElementsParents(el, check) {
if (el.parentElement) { if (el.parentElement) {
if (!check(el.parentElement)) { if (!check(el.parentElement)) {
return checkElementsParents(el.parentElement, check); return checkElementsParents(el.parentElement, check);
} else { } else {
return true; return true;
} }
} else { } else {
return false; return false;
} }
} }
(() => {
// If user has linked to a heading that's inside of a tab
const hash = window.location.hash;
if (!hash) return;
const heading = document.querySelector < HTMLElement > (hash);
if (!heading) return;
const isHidden = checkElementsParents(heading, el =>
el.hasAttribute('hidden') && el.getAttribute('hidden') !== "false"
)
// If it's not hidden, then we can assume that the browser will auto-scroll to it
if (!isHidden) return;
const partialHash = hash.slice(1);
try {
const matchingTab = document.querySelector < HTMLElement > (
`[data-headers*="${partialHash}"`
);
if (!matchingTab) return;
// If header is not in a tab
const tabName = matchingTab.getAttribute("data-tabname");
if (!tabName) return;
matchingTab.click();
setTimeout(() => {
const el = document.querySelector(hash);
if (!el) return;
el.scrollIntoView(true);
}, 0);
} catch (e) {
console.error("Error finding matching tab", e);
}
})()
(() => {
// If user has linked to a heading that's inside of a tab
const hash = window.location.hash;
if (!hash) return;
const heading = document.querySelector < HTMLElement > hash;
if (!heading) return;
const isHidden = checkElementsParents(
heading,
(el) => el.hasAttribute("hidden") && el.getAttribute("hidden") !== "false"
);
// If it's not hidden, then we can assume that the browser will auto-scroll to it
if (!isHidden) return;
const partialHash = hash.slice(1);
try {
const matchingTab =
document.querySelector <
HTMLElement >
`[data-headers*="${partialHash}"`;
if (!matchingTab) return;
// If header is not in a tab
const tabName = matchingTab.getAttribute("data-tabname");
if (!tabName) return;
matchingTab.click();
setTimeout(() => {
const el = document.querySelector(hash);
if (!el) return;
el.scrollIntoView(true);
}, 0);
} catch (e) {
console.error("Error finding matching tab", e);
}
})();
}); });

View File

@@ -1,28 +1,29 @@
/* AFTER CHANGING THIS FILE, PLEASE MANUALLY MINIFY IT AND PUT INTO tabs.min.js */ /* AFTER CHANGING THIS FILE, PLEASE MANUALLY MINIFY IT AND PUT INTO tabs.min.js */
const COLOR_MODE_STORAGE_KEY = "currentTheme"; const COLOR_MODE_STORAGE_KEY = "currentTheme";
const themeToggleBtn = document.querySelector('#theme-toggle-button'); const themeToggleBtn = document.querySelector("#theme-toggle-button");
const darkIconEl = document.querySelector('#dark-icon'); const darkIconEl = document.querySelector("#dark-icon");
const lightIconEl = document.querySelector('#light-icon'); const lightIconEl = document.querySelector("#light-icon");
function toggleButton(theme) { function toggleButton(theme) {
themeToggleBtn.ariaPressed = `${theme === 'dark'}`; themeToggleBtn.ariaPressed = `${theme === "dark"}`;
if (theme === 'light') { if (theme === "light") {
lightIconEl.style.display = null; lightIconEl.style.display = null;
darkIconEl.style.display = 'none'; darkIconEl.style.display = "none";
} else { } else {
lightIconEl.style.display = 'none'; lightIconEl.style.display = "none";
darkIconEl.style.display = null; darkIconEl.style.display = null;
} }
} }
// TODO: Migrate to `classList` // TODO: Migrate to `classList`
const initialTheme = document.documentElement.className; const initialTheme = document.documentElement.className;
toggleButton(initialTheme); toggleButton(initialTheme);
themeToggleBtn.addEventListener('click', () => { themeToggleBtn.addEventListener("click", () => {
const currentTheme = document.documentElement.className; const currentTheme = document.documentElement.className;
document.documentElement.className = currentTheme === 'light' ? 'dark' : 'light'; document.documentElement.className =
// TODO: Persist new setting currentTheme === "light" ? "dark" : "light";
const newTheme = document.documentElement.className; // TODO: Persist new setting
toggleButton(newTheme); const newTheme = document.documentElement.className;
localStorage.setItem(COLOR_MODE_STORAGE_KEY, newTheme) toggleButton(newTheme);
}) localStorage.setItem(COLOR_MODE_STORAGE_KEY, newTheme);
});

View File

@@ -1,14 +1,14 @@
self.addEventListener("install", function (e) { self.addEventListener("install", function (e) {
self.skipWaiting(); self.skipWaiting();
}); });
self.addEventListener("activate", function (e) { self.addEventListener("activate", function (e) {
self.registration self.registration
.unregister() .unregister()
.then(function () { .then(function () {
return self.clients.matchAll(); return self.clients.matchAll();
}) })
.then(function (clients) { .then(function (clients) {
clients.forEach((client) => client.navigate(client.url)); clients.forEach((client) => client.navigate(client.url));
}); });
}); });

View File

@@ -1,10 +1,10 @@
if ("serviceWorker" in navigator) { if ("serviceWorker" in navigator) {
navigator.serviceWorker navigator.serviceWorker
.register("/sw.js") .register("/sw.js")
.then(serviceWorker => { .then((serviceWorker) => {
console.log("Service Worker registered: ", serviceWorker); console.log("Service Worker registered: ", serviceWorker);
}) })
.catch(error => { .catch((error) => {
console.error("Error registering the Service Worker: ", error); console.error("Error registering the Service Worker: ", error);
}); });
} }

View File

@@ -3,13 +3,13 @@ import layoutStyles from "./blog-post-layout.module.scss";
--- ---
<div class={layoutStyles.blogPostLayoutContainer}> <div class={layoutStyles.blogPostLayoutContainer}>
<div class={layoutStyles.leftContainer}> <div class={layoutStyles.leftContainer}>
<slot name="left"/> <slot name="left" />
</div> </div>
<div class={layoutStyles.centerContainer}> <div class={layoutStyles.centerContainer}>
<slot/> <slot />
</div> </div>
<div class={layoutStyles.rightContainer}> <div class={layoutStyles.rightContainer}>
<slot name="right"/> <slot name="right" />
</div> </div>
</div> </div>

View File

@@ -1,16 +1,28 @@
--- ---
import { Icon } from 'astro-icon'; import { Icon } from "astro-icon";
import btnStyles from "./dark-light-button.module.scss"; import btnStyles from "./dark-light-button.module.scss";
--- ---
<button <button
class={`${btnStyles.darkLightBtn} baseBtn`} class={`${btnStyles.darkLightBtn} baseBtn`}
id="theme-toggle-button" id="theme-toggle-button"
aria-pressed="false" aria-pressed="false"
aria-label={"Is dark mode enabled?"} aria-label={"Is dark mode enabled?"}
> >
<Icon name="dark" height="36" width="36" id="dark-icon" style="display: none;"/> <Icon
<Icon name="light" height="36" width="36" id="light-icon" style="display: none;"/> name="dark"
height="36"
width="36"
id="dark-icon"
style="display: none;"
/>
<Icon
name="light"
height="36"
width="36"
id="light-icon"
style="display: none;"
/>
</button> </button>
<script defer is:inline src="/scripts/themetoggle.min.js"/> <script defer is:inline src="/scripts/themetoggle.min.js"></script>

View File

@@ -1,7 +1,7 @@
--- ---
import layoutStyles from "./layout.module.scss"; import layoutStyles from "./layout.module.scss";
import DarkLightButton from "components/dark-light-button/dark-light-button.astro"; import DarkLightButton from "components/dark-light-button/dark-light-button.astro";
import { Icon } from 'astro-icon'; import { Icon } from "astro-icon";
// const { back } = useHistory(); // const { back } = useHistory();
@@ -10,30 +10,46 @@ const rootPath = `/`;
const isBase = Astro.url.pathname === rootPath; const isBase = Astro.url.pathname === rootPath;
const isBlogPost = Astro.url.pathname.startsWith(`${rootPath}posts`); const isBlogPost = Astro.url.pathname.startsWith(`${rootPath}posts`);
const isCollection = Astro.url.pathname.startsWith(`${rootPath}collections`); const isCollection = Astro.url.pathname.startsWith(`${rootPath}collections`);
--- ---
<div class={layoutStyles.horizCenter}> <div class={layoutStyles.horizCenter}>
<header class={layoutStyles.header} aria-label={"Toolbar for primary action buttons"}> <header
<div class={layoutStyles.headerInsideContainer}> class={layoutStyles.header}
{!isBase ? (<> aria-label={"Toolbar for primary action buttons"}
<button id="backbtn" class={`${layoutStyles.backBtn} baseBtn`} aria-label="Go back"> >
<Icon height="36" width="36" name="back" /> <div class={layoutStyles.headerInsideContainer}>
</button> {
<script is:inline defer src="/scripts/backbtn.min.js"/> !isBase ? (
</> <>
) : ( <button
<div /> id="backbtn"
)} class={`${layoutStyles.backBtn} baseBtn`}
<div class={layoutStyles.iconList}> aria-label="Go back"
<!-- <AnalyticsLink category={"outbound"} href="https://discord.gg/FMcvc6T" className={"baseBtn"} >
<Icon height="36" width="36" name="back" />
</button>
<script is:inline defer src="/scripts/backbtn.min.js" />
</>
) : (
<div />
)
}
<div class={layoutStyles.iconList}>
<!-- <AnalyticsLink category={"outbound"} href="https://discord.gg/FMcvc6T" className={"baseBtn"}
aria-label={"Join the Discord"}> --> aria-label={"Join the Discord"}> -->
<Icon height="36" width="36" name="discord" /> <Icon height="36" width="36" name="discord" />
<!-- </AnalyticsLink> --> <!-- </AnalyticsLink> -->
<DarkLightButton /> <DarkLightButton />
</div> </div>
</div> </div>
</header> </header>
<div class={ isCollection ? "" : !isBlogPost ? "listViewContent" : "postViewContent" }> <div
<slot /> class={isCollection
</div> ? ""
: !isBlogPost
? "listViewContent"
: "postViewContent"}
>
<slot />
</div>
</div> </div>

View File

@@ -1,112 +1,112 @@
export interface Page { export interface Page {
display: string; display: string;
pageNumber: number; pageNumber: number;
ariaLabel?: string; ariaLabel?: string;
} }
export const DR = { export const DR = {
ariaLabel: "Go to the next set of pages", ariaLabel: "Go to the next set of pages",
display: '...' display: "...",
}; };
export const DL = { export const DL = {
ariaLabel: "Go to previous set of pages", ariaLabel: "Go to previous set of pages",
display: '...' display: "...",
}; };
const range = (start: number, end: number): Page[] => { const range = (start: number, end: number): Page[] => {
let length = end - start + 1; const length = end - start + 1;
return Array.from({ length }, (_, idx) => { return Array.from({ length }, (_, idx) => {
const page = idx + start; const page = idx + start;
return { return {
display: String(page), display: String(page),
pageNumber: page, pageNumber: page,
ariaLabel: `Goto page ${page}` ariaLabel: `Goto page ${page}`,
} };
}); });
}; };
interface GetPaginationRangeProps { interface GetPaginationRangeProps {
totalCount: number; totalCount: number;
pageSize: number; pageSize: number;
siblingCount?: number; siblingCount?: number;
currentPage: number; currentPage: number;
} }
export const getPaginationRange = ({ export const getPaginationRange = ({
totalCount, totalCount,
pageSize, pageSize,
siblingCount = 1, siblingCount = 1,
currentPage currentPage,
}: GetPaginationRangeProps): Page[] => { }: GetPaginationRangeProps): Page[] => {
const totalPageCount = Math.ceil(totalCount / pageSize); const totalPageCount = Math.ceil(totalCount / pageSize);
const totalPageCountPage: Page = { const totalPageCountPage: Page = {
display: `${totalPageCount}`, display: `${totalPageCount}`,
pageNumber: totalPageCount pageNumber: totalPageCount,
} };
const totalPageNumbers = siblingCount + 5; const totalPageNumbers = siblingCount + 5;
if (totalPageNumbers >= totalPageCount) { if (totalPageNumbers >= totalPageCount) {
return range(1, totalPageCount); return range(1, totalPageCount);
} }
const leftSiblingIndex = Math.max(currentPage - siblingCount, 1); const leftSiblingIndex = Math.max(currentPage - siblingCount, 1);
const rightSiblingIndex = Math.min( const rightSiblingIndex = Math.min(
currentPage + siblingCount, currentPage + siblingCount,
totalPageCount totalPageCount
); );
const shouldShowLeftDots = leftSiblingIndex > 2; const shouldShowLeftDots = leftSiblingIndex > 2;
const shouldShowRightDots = rightSiblingIndex < totalPageCount - 2; const shouldShowRightDots = rightSiblingIndex < totalPageCount - 2;
const firstPageIndex: Page = { const firstPageIndex: Page = {
display: "1", display: "1",
pageNumber: 1 pageNumber: 1,
}; };
const lastPageIndex: Page = { const lastPageIndex: Page = {
display: `${totalPageCount}`, display: `${totalPageCount}`,
pageNumber: totalPageCount pageNumber: totalPageCount,
}; };
if (!shouldShowLeftDots && shouldShowRightDots) { if (!shouldShowLeftDots && shouldShowRightDots) {
let leftItemCount = 3 + 2 * siblingCount; const leftItemCount = 3 + 2 * siblingCount;
let leftRange = range(1, leftItemCount); const leftRange = range(1, leftItemCount);
const lastPage = leftRange[leftRange.length - 1] const lastPage = leftRange[leftRange.length - 1];
const DR_Page: Page = { const DR_Page: Page = {
...DR, ...DR,
pageNumber: currentPage + 2 pageNumber: currentPage + 2,
} };
return [...leftRange, DR_Page, totalPageCountPage]; return [...leftRange, DR_Page, totalPageCountPage];
} }
if (shouldShowLeftDots && !shouldShowRightDots) { if (shouldShowLeftDots && !shouldShowRightDots) {
let rightItemCount = 3 + 2 * siblingCount; const rightItemCount = 3 + 2 * siblingCount;
let rightRange = range( const rightRange = range(
totalPageCount - rightItemCount + 1, totalPageCount - rightItemCount + 1,
totalPageCount totalPageCount
); );
const DL_Page: Page = { const DL_Page: Page = {
...DL, ...DL,
pageNumber: currentPage - 2 pageNumber: currentPage - 2,
} };
return [firstPageIndex, DL_Page, ...rightRange]; return [firstPageIndex, DL_Page, ...rightRange];
} }
if (shouldShowLeftDots && shouldShowRightDots) { if (shouldShowLeftDots && shouldShowRightDots) {
let middleRange = range(leftSiblingIndex, rightSiblingIndex); const middleRange = range(leftSiblingIndex, rightSiblingIndex);
const DL_Page: Page = { const DL_Page: Page = {
...DL, ...DL,
pageNumber: currentPage - 2 pageNumber: currentPage - 2,
} };
const DR_Page: Page = { const DR_Page: Page = {
...DR, ...DR,
pageNumber: currentPage + 2 pageNumber: currentPage + 2,
} };
return [firstPageIndex, DL_Page, ...middleRange, DR_Page, lastPageIndex]; return [firstPageIndex, DL_Page, ...middleRange, DR_Page, lastPageIndex];
} }
return []; return [];
}; };

View File

@@ -1,31 +1,31 @@
--- ---
import { getPaginationRange } from './pagination-logic'; import { getPaginationRange } from "./pagination-logic";
import { Page } from 'astro'; import { Page } from "astro";
import { PostInfo } from 'types/PostInfo'; import { PostInfo } from "types/PostInfo";
import styles from './pagination.module.scss'; import styles from "./pagination.module.scss";
interface PaginationProps { interface PaginationProps {
page: Pick<Page<PostInfo>, 'total' | 'currentPage' | 'size' | 'lastPage' | 'url'>; page: Pick<
class: string; Page<PostInfo>,
rootURL: string; "total" | "currentPage" | "size" | "lastPage" | "url"
>;
class: string;
rootURL: string;
} }
const { const { page, rootURL, class: className = "" } = Astro.props as PaginationProps;
page,
rootURL,
class: className = ""
} = Astro.props as PaginationProps;
const paginationRange = getPaginationRange({ const paginationRange = getPaginationRange({
currentPage: page.currentPage, currentPage: page.currentPage,
totalCount: page.total, totalCount: page.total,
siblingCount: 0, siblingCount: 0,
pageSize: page.size pageSize: page.size,
}); });
const dontShowAnything = page.currentPage === 0 || paginationRange.length < 2; const dontShowAnything = page.currentPage === 0 || paginationRange.length < 2;
const getPageHref = (pageNum: number) => pageNum === 0 || pageNum === 1 ? rootURL : `${rootURL}page/${pageNum}`; const getPageHref = (pageNum: number) =>
pageNum === 0 || pageNum === 1 ? rootURL : `${rootURL}page/${pageNum}`;
const lastPage = paginationRange[paginationRange.length - 1]; const lastPage = paginationRange[paginationRange.length - 1];
const firstPage = paginationRange[0]; const firstPage = paginationRange[0];
@@ -34,27 +34,47 @@ const disablePrevious = !firstPage || page.currentPage === firstPage.pageNumber;
const disableNext = !lastPage || page.currentPage === lastPage.pageNumber; const disableNext = !lastPage || page.currentPage === lastPage.pageNumber;
--- ---
{dontShowAnything ? null : <ul role="navigation" aria-label="Pagination Navigation" class={`${styles.pagination} ${className}`}> {
{!disablePrevious && <li class={`${styles.paginationItem} ${styles.previous}`}> dontShowAnything ? null : (
<a href={getPageHref(page.currentPage - 1)} aria-label="Previous"> <ul
{"<"} role="navigation"
</a> aria-label="Pagination Navigation"
</li>} class={`${styles.pagination} ${className}`}
>
{!disablePrevious && (
<li class={`${styles.paginationItem} ${styles.previous}`}>
<a href={getPageHref(page.currentPage - 1)} aria-label="Previous">
{"<"}
</a>
</li>
)}
{paginationRange.map(pageItem => { {paginationRange.map((pageItem) => {
const isSelected = pageItem.pageNumber === page.currentPage; const isSelected = pageItem.pageNumber === page.currentPage;
return ( return (
<li class={`${styles.paginationItem} ${ isSelected ? styles.active : '' }`}> <li
<a href={getPageHref(pageItem.pageNumber)} aria-label={pageItem.ariaLabel} aria-current={isSelected || undefined}> class={`${styles.paginationItem} ${
{pageItem.display} isSelected ? styles.active : ""
</a> }`}
</li> >
); <a
})} href={getPageHref(pageItem.pageNumber)}
aria-label={pageItem.ariaLabel}
aria-current={isSelected || undefined}
>
{pageItem.display}
</a>
</li>
);
})}
{!disableNext && <li class={`${styles.paginationItem} ${styles.next}`}> {!disableNext && (
<a href={getPageHref(page.currentPage + 1)} aria-label="Next"> <li class={`${styles.paginationItem} ${styles.next}`}>
{">"} <a href={getPageHref(page.currentPage + 1)} aria-label="Next">
</a> {">"}
</li>} </a>
</ul>} </li>
)}
</ul>
)
}

View File

@@ -5,12 +5,12 @@ import { PostInfo } from "types/PostInfo";
// import { PostListContext } from "constants/post-list-context"; // import { PostListContext } from "constants/post-list-context";
export interface PostListProps { export interface PostListProps {
showWordCount?: boolean; showWordCount?: boolean;
numberOfArticles?: number; numberOfArticles?: number;
wordCount?: number; wordCount?: number;
unicornData?: PostInfo['authorsMeta']; unicornData?: PostInfo["authorsMeta"];
listAriaLabel: string; listAriaLabel: string;
postsToDisplay: PostInfo[] postsToDisplay: PostInfo[];
} }
/** /**
* unicornData - The data with the associated post. If present - you're on profile page * unicornData - The data with the associated post. If present - you're on profile page
@@ -18,12 +18,7 @@ export interface PostListProps {
const { listAriaLabel, postsToDisplay } = Astro.props as PostListProps; const { listAriaLabel, postsToDisplay } = Astro.props as PostListProps;
--- ---
<ul
class={listStyle.postsListContainer} <ul class={listStyle.postsListContainer} aria-label={listAriaLabel} role="list">
aria-label={listAriaLabel} {postsToDisplay.map((post) => <PostCard post={post} />)}
role="list"
>
{postsToDisplay.map((post) => (
<PostCard post={post}/>
))}
</ul> </ul>

View File

@@ -7,61 +7,52 @@ import dayjs from "dayjs";
import { PostInfo } from "types/PostInfo"; import { PostInfo } from "types/PostInfo";
interface PostCardProps { interface PostCardProps {
post: PostInfo; // Info on the authors of the post post: PostInfo; // Info on the authors of the post
class?: string; // class to pass to the post card element class?: string; // class to pass to the post card element
} }
const { const { post, class: className = "" } = Astro.props as PostCardProps;
post,
class: className = ""
} = Astro.props as PostCardProps;
const { const { published, slug, title, authorsMeta, tags, description, excerpt } =
published, post;
slug,
title,
authorsMeta,
tags,
description,
excerpt
} = post;
const publishedStr = dayjs(published).format("MMMM D, YYYY"); const publishedStr = dayjs(published).format("MMMM D, YYYY");
--- ---
<li class={`${cardStyles.card} ${className}`} role="listitem"> <li class={`${cardStyles.card} ${className}`} role="listitem">
<div class={cardStyles.cardContents}> <div class={cardStyles.cardContents}>
<a href={`/posts/${slug}`} class="unlink"> <a href={`/posts/${slug}`} class="unlink">
<h2 class={cardStyles.header}> <h2 class={cardStyles.header}>
{title} {title}
</h2> </h2>
</a> </a>
<p class={cardStyles.authorName}> <p class={cardStyles.authorName}>
<span>by&nbsp;</span> <span>by&nbsp;</span>
<a class={cardStyles.authorLink} href={`/unicorns/${authorsMeta[0].id}`}> <a class={cardStyles.authorLink} href={`/unicorns/${authorsMeta[0].id}`}>
{authorsMeta[0].name} {authorsMeta[0].name}
</a> </a>
<!-- To avoid having commas on the first author name, we did this --> <!-- To avoid having commas on the first author name, we did this -->
{authorsMeta.slice(1).map((author, i) => { {
return ( authorsMeta.slice(1).map((author, i) => {
<> return (
<span>, </span> <>
<a href={`/unicorns/${author.id}`} class={cardStyles.authorLink}> <span>, </span>
{author.name} <a href={`/unicorns/${author.id}`} class={cardStyles.authorLink}>
</a> {author.name}
</> </a>
); </>
})} );
</p> })
<div class={cardStyles.dateTagSubheader}> }
<p class={cardStyles.date}>{publishedStr}</p> </p>
{tags.map((tag) => ( <div class={cardStyles.dateTagSubheader}>
<span class={cardStyles.tag}> <p class={cardStyles.date}>{publishedStr}</p>
{tag} {tags.map((tag) => <span class={cardStyles.tag}>{tag}</span>)}
</span> </div>
))} <p class={cardStyles.excerpt} set:html={description || excerpt}></p>
</div> </div>
<p class={cardStyles.excerpt} set:html={description || excerpt} /> <UserProfilePic
</div> authors={authorsMeta}
<UserProfilePic authors={authorsMeta} className={cardStyles.authorImagesContainer} /> className={cardStyles.authorImagesContainer}
/>
</li> </li>

View File

@@ -1,19 +1,33 @@
--- ---
import {SEOProps} from './shared'; import { SEOProps } from "./shared";
type Props = Pick<SEOProps, 'editedTime' | 'publishedTime' | 'keywords' | 'unicornsData'>; type Props = Pick<
const { keywords, editedTime, publishedTime, unicornsData } = Astro.props as Props; SEOProps,
const author = unicornsData.map(uni => uni.name).join(","); "editedTime" | "publishedTime" | "keywords" | "unicornsData"
>;
const { keywords, editedTime, publishedTime, unicornsData } =
Astro.props as Props;
const author = unicornsData.map((uni) => uni.name).join(",");
--- ---
<>{keywords?.length ? keywords.map(keyword => <>
<meta property="article:tag" content={keyword} />) : null} {
keywords?.length
? keywords.map((keyword) => (
<meta property="article:tag" content={keyword} />
))
: null
}
</> </>
<meta property="article:section" content="Technology" /> <meta property="article:section" content="Technology" />
<meta property="article:author" content={author}/> <meta property="article:author" content={author} />
<>{editedTime && <>
<meta property="article:modified_time" content={editedTime} />} {editedTime && <meta property="article:modified_time" content={editedTime} />}
</> </>
<>{publishedTime && <>
<meta property="article:published_time" content={publishedTime} />} {
publishedTime && (
<meta property="article:published_time" content={publishedTime} />
)
}
</> </>

View File

@@ -1,15 +1,11 @@
--- ---
import {SEOProps} from './shared'; import { SEOProps } from "./shared";
type Props = Pick<SEOProps, 'publishedTime' | 'isbn' | 'unicornsData'>; type Props = Pick<SEOProps, "publishedTime" | "isbn" | "unicornsData">;
const { const { publishedTime, unicornsData, isbn } = Astro.props as Props;
publishedTime,
unicornsData,
isbn
} = Astro.props as Props;
const author = unicornsData!.map((uni)=> uni.name).join(","); const author = unicornsData!.map((uni) => uni.name).join(",");
--- ---
<meta property="book:release_date" content={publishedTime} /> <meta property="book:release_date" content={publishedTime} />

View File

@@ -1,34 +1,42 @@
--- ---
import { removePrefixLanguageFromPath } from "utils/translations";
import { SEOProps } from "./shared";
import { removePrefixLanguageFromPath } from 'utils/translations'; type Props = Pick<SEOProps, "langData"> & {
import {SEOProps} from './shared'; pathName: string;
siteMetadata: { siteUrl: string };
type Props = Pick<SEOProps, 'langData'> & {
pathName: string;
siteMetadata: {siteUrl: string}
}; };
const {langData, siteMetadata, pathName} = Astro.props as Props; const { langData, siteMetadata, pathName } = Astro.props as Props;
--- ---
{langData?.currentLang && (
<link
rel="alternate"
href={
siteMetadata.siteUrl + removePrefixLanguageFromPath(pathName || "")
}
href-lang="x-default"
/>
)}
{langData?.otherLangs?.length ?
langData.otherLangs.map((lang) => (
<link
rel="alternate"
href={
siteMetadata.siteUrl +
`${lang === "en" ? "" : "/"}${lang === "en" ? "" : lang}` +
removePrefixLanguageFromPath(pathName || "")
}
href-lang={lang}
/>
)) : null}
<>
{
langData?.currentLang && (
<link
rel="alternate"
href={
siteMetadata.siteUrl + removePrefixLanguageFromPath(pathName || "")
}
href-lang="x-default"
/>
)
}
</>
<>
{
langData?.otherLangs?.length
? langData.otherLangs.map((lang) => (
<link
rel="alternate"
href={
siteMetadata.siteUrl +
`${lang === "en" ? "" : "/"}${lang === "en" ? "" : lang}` +
removePrefixLanguageFromPath(pathName || "")
}
href-lang={lang}
/>
))
: null
}
</>

View File

@@ -1,44 +1,48 @@
--- ---
import { fileToOpenGraphConverter } from "utils/translations"; import { fileToOpenGraphConverter } from "utils/translations";
import {SEOProps} from './shared'; import { SEOProps } from "./shared";
type Props = Pick<SEOProps, 'title' | 'langData' | 'unicornsData'> & { type Props = Pick<SEOProps, "title" | "langData" | "unicornsData"> & {
currentPath: string; currentPath: string;
metaDescription: string; metaDescription: string;
metaImage: string; metaImage: string;
ogType: string; ogType: string;
siteMetadata: {title: string} siteMetadata: { title: string };
}; };
const { const {
currentPath, currentPath,
siteMetadata, siteMetadata,
title, title,
langData, langData,
metaDescription, metaDescription,
metaImage, metaImage,
ogType, ogType,
} = Astro.props as Props; } = Astro.props as Props;
--- ---
{/* Open Graph SEO */} {/* Open Graph SEO */}
<meta property="og:url" content={currentPath} /> <meta property="og:url" content={currentPath} />
<meta property="og:site_name" content={siteMetadata.title} /> <meta property="og:site_name" content={siteMetadata.title} />
<meta property="og:title" content={title} /> <meta property="og:title" content={title} />
<meta <meta
property="og:locale" property="og:locale"
content={ content={langData ? fileToOpenGraphConverter(langData.currentLang) : "en"}
langData ? fileToOpenGraphConverter(langData.currentLang) : "en"
}
/> />
{langData?.otherLangs?.length ? <>
langData.otherLangs.map((lang) => ( {
<meta langData?.otherLangs?.length
property="og:locale:alternate" ? langData.otherLangs.map((lang) => (
content={fileToOpenGraphConverter(lang)} <meta
/> property="og:locale:alternate"
)) : null} content={fileToOpenGraphConverter(lang)}
/>
))
: null
}
</>
<meta property="og:description" content={metaDescription} /> <meta property="og:description" content={metaDescription} />
<meta property="og:image" content={metaImage} /> <meta property="og:image" content={metaImage} />
<meta property="og:type" content={ogType} /> <meta property="og:type" content={ogType} />

View File

@@ -1,7 +1,7 @@
--- ---
import {SEOProps} from './shared'; import { SEOProps } from "./shared";
type Props = Pick<SEOProps, 'unicornsData'>; type Props = Pick<SEOProps, "unicornsData">;
const { unicornsData } = Astro.props as Props; const { unicornsData } = Astro.props as Props;
--- ---

View File

@@ -10,18 +10,18 @@ import { SEOProps } from "./shared";
import Twitter from "./twitter.astro"; import Twitter from "./twitter.astro";
const { const {
description = "", description = "",
title, title,
keywords, keywords,
canonical, canonical,
type, type,
unicornsData, unicornsData,
publishedTime, publishedTime,
editedTime, editedTime,
pathName, pathName,
isbn, isbn,
langData, langData,
shareImage, shareImage,
} = Astro.props as SEOProps; } = Astro.props as SEOProps;
const metaDescription = description || siteMetadata.description; const metaDescription = description || siteMetadata.description;
@@ -32,29 +32,62 @@ const ogType = type ?? "blog";
const socialUnicorn = unicornsData?.find((uni) => uni.socials); const socialUnicorn = unicornsData?.find((uni) => uni.socials);
const uniTwitter = const uniTwitter =
socialUnicorn && socialUnicorn.socials && socialUnicorn.socials.twitter; socialUnicorn && socialUnicorn.socials && socialUnicorn.socials.twitter;
const currentPath = siteMetadata.siteUrl + (pathName || ""); const currentPath = siteMetadata.siteUrl + (pathName || "");
--- ---
<title> <title>
{title ? `${title} | ${siteMetadata.title}` : siteMetadata.title} {title ? `${title} | ${siteMetadata.title}` : siteMetadata.title}
</title> </title>
{canonical ? <>{canonical ? <link rel="canonical" href={canonical} /> : null}</>
<link rel="canonical" href={canonical} /> : null}
<meta property="name" content={siteMetadata.title} /> <meta property="name" content={siteMetadata.title} />
<meta name="description" content={metaDescription} /> <meta name="description" content={metaDescription} />
<meta property="keywords" content={metaKeywords} /> <meta property="keywords" content={metaKeywords} />
<Analytics /> <Analytics />
<Twitter title={title} metaDescription={metaDescription} siteMetadata={siteMetadata} metaImage={metaImage} <Twitter
unicornsData={unicornsData} uniTwitter={uniTwitter} type={type} /> title={title}
<OpenGraph currentPath={currentPath} siteMetadata={siteMetadata} title={title} langData={langData} metaDescription={metaDescription}
metaDescription={metaDescription} metaImage={metaImage} ogType={ogType} /> siteMetadata={siteMetadata}
metaImage={metaImage}
unicornsData={unicornsData}
uniTwitter={uniTwitter}
type={type}
/>
<OpenGraph
currentPath={currentPath}
siteMetadata={siteMetadata}
title={title}
langData={langData}
metaDescription={metaDescription}
metaImage={metaImage}
ogType={ogType}
/>
<Locale langData={langData} siteMetadata={siteMetadata} pathName={pathName} /> <Locale langData={langData} siteMetadata={siteMetadata} pathName={pathName} />
{type === 'article' && <>
<Article keywords={keywords} editedTime={editedTime} publishedTime={publishedTime} unicornsData={unicornsData} />} {
{type === 'book' && type === "article" && (
<Book publishedTime={publishedTime} unicornsData={unicornsData} isbn={isbn} />} <Article
{type === 'profile' && keywords={keywords}
<Profile unicornsData={unicornsData} />} editedTime={editedTime}
publishedTime={publishedTime}
unicornsData={unicornsData}
/>
)
}
</>
<>
{
type === "book" && (
<Book
publishedTime={publishedTime}
unicornsData={unicornsData}
isbn={isbn}
/>
)
}
</>
<>
{type === "profile" && <Profile unicornsData={unicornsData} />}
</>
<slot /> <slot />

View File

@@ -1,19 +1,19 @@
import { Languages, UnicornInfo } from "../../types"; import { Languages, UnicornInfo } from "../../types";
export interface SEOProps { export interface SEOProps {
description?: string; description?: string;
langData?: { langData?: {
currentLang: Languages; currentLang: Languages;
otherLangs: Languages[]; otherLangs: Languages[];
}; };
title: string; title: string;
unicornsData?: UnicornInfo[]; unicornsData?: UnicornInfo[];
keywords?: string[]; keywords?: string[];
publishedTime?: string; publishedTime?: string;
editedTime?: string; editedTime?: string;
type?: "article" | "profile" | "book"; type?: "article" | "profile" | "book";
pathName?: string; pathName?: string;
canonical?: string; canonical?: string;
isbn?: string; isbn?: string;
shareImage?: string; shareImage?: string;
} }

View File

@@ -1,29 +1,34 @@
--- ---
import {SEOProps} from './shared'; import { SEOProps } from "./shared";
type Props = Pick<SEOProps, 'title' | 'unicornsData' | 'type'> & { type Props = Pick<SEOProps, "title" | "unicornsData" | "type"> & {
metaDescription: string; metaDescription: string;
metaImage: string; metaImage: string;
siteMetadata: {twitterHandle: string} siteMetadata: { twitterHandle: string };
uniTwitter?: string; uniTwitter?: string;
}; };
const { const {
title, title,
metaDescription, metaDescription,
siteMetadata, siteMetadata,
metaImage, metaImage,
unicornsData, unicornsData,
uniTwitter, uniTwitter,
type type,
} = Astro.props as Props; } = Astro.props as Props;
--- ---
{/* Twitter SEO */} {/* Twitter SEO */}
<meta name="twitter:title" content={title} /> <meta name="twitter:title" content={title} />
<meta name="twitter:description" content={metaDescription} /> <meta name="twitter:description" content={metaDescription} />
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content={siteMetadata.twitterHandle} /> <meta name="twitter:site" content={siteMetadata.twitterHandle} />
<meta name="twitter:image" content={metaImage} /> <meta name="twitter:image" content={metaImage} />
{type === "article" && unicornsData?.length === 1 && uniTwitter ? ( <>
<meta property="twitter:creator" content={`@${uniTwitter}`} /> {
) : null} type === "article" && unicornsData?.length === 1 && uniTwitter ? (
<meta property="twitter:creator" content={`@${uniTwitter}`} />
) : null
}
</>

View File

@@ -1,68 +1,71 @@
--- ---
const {headingsToDisplaySlugs} = Astro.props as {headingsToDisplaySlugs: Array<string>} const { headingsToDisplaySlugs } = Astro.props as {
headingsToDisplaySlugs: Array<string>;
};
--- ---
<script define:vars={{headingsToDisplaySlugs}}> <script define:vars={{ headingsToDisplaySlugs }}>
window.onload = () => { window.onload = () => {
const tocListRef = document.querySelector('#tocList'); const tocListRef = document.querySelector("#tocList");
const linkRefs = [...document.querySelectorAll('[data-headingitem="true"]')] const linkRefs = [
...document.querySelectorAll('[data-headingitem="true"]'),
];
let previousSection = {current: ""} let previousSection = { current: "" };
const handleObserver = (entries) => { const handleObserver = (entries) => {
const highlightFirstActive = () => { const highlightFirstActive = () => {
if (!tocListRef) return; if (!tocListRef) return;
let firstVisibleLink = let firstVisibleLink = tocListRef.querySelector(".toc-is-visible");
tocListRef.querySelector(".toc-is-visible");
linkRefs.forEach((linkRef) => { linkRefs.forEach((linkRef) => {
linkRef.classList.remove("toc-is-active"); linkRef.classList.remove("toc-is-active");
}); });
if (firstVisibleLink) { if (firstVisibleLink) {
firstVisibleLink.classList.add("toc-is-active"); firstVisibleLink.classList.add("toc-is-active");
} }
if (!firstVisibleLink && previousSection.current) { if (!firstVisibleLink && previousSection.current) {
tocListRef tocListRef
.querySelector(`a[href="#${previousSection.current}"]`) .querySelector(`a[href="#${previousSection.current}"]`)
.parentElement.classList.add("toc-is-active"); .parentElement.classList.add("toc-is-active");
} }
}; };
entries.forEach((entry) => { entries.forEach((entry) => {
let href = `#${entry.target.getAttribute("id")}`, let href = `#${entry.target.getAttribute("id")}`,
link = linkRefs.find( link = linkRefs.find(
(l) => l.firstElementChild.getAttribute("href") === href (l) => l.firstElementChild.getAttribute("href") === href
); );
if (!link) return; if (!link) return;
if (entry.isIntersecting && entry.intersectionRatio >= 1) { if (entry.isIntersecting && entry.intersectionRatio >= 1) {
link.classList.add("toc-is-visible"); link.classList.add("toc-is-visible");
const newPreviousSection = entry.target.getAttribute("id"); const newPreviousSection = entry.target.getAttribute("id");
previousSection.current = newPreviousSection; previousSection.current = newPreviousSection;
} else { } else {
link.classList.remove("toc-is-visible"); link.classList.remove("toc-is-visible");
} }
highlightFirstActive(); highlightFirstActive();
}); });
}; };
const observer = new IntersectionObserver(handleObserver, { const observer = new IntersectionObserver(handleObserver, {
rootMargin: "0px", rootMargin: "0px",
threshold: 1, threshold: 1,
}); });
const headingsEls = headingsToDisplaySlugs.map((headingToDisplay) => { const headingsEls = headingsToDisplaySlugs.map((headingToDisplay) => {
return document.getElementById(headingToDisplay); return document.getElementById(headingToDisplay);
}); });
headingsEls headingsEls
.filter((a) => a) .filter((a) => a)
.forEach((heading) => { .forEach((heading) => {
observer.observe(heading); observer.observe(heading);
}); });
} };
</script> </script>

View File

@@ -6,7 +6,7 @@ import { PostInfo } from "types/PostInfo";
import HeadingIntersectionObserverScript from "./heading-intersection-observer-script.astro"; import HeadingIntersectionObserverScript from "./heading-intersection-observer-script.astro";
interface TableOfContentsProps { interface TableOfContentsProps {
headingsWithId: PostInfo["headingsWithId"]; headingsWithId: PostInfo["headingsWithId"];
} }
const { headingsWithId } = Astro.props as TableOfContentsProps; const { headingsWithId } = Astro.props as TableOfContentsProps;
@@ -18,33 +18,30 @@ const minDepth = Math.min(...headings.map((h) => h.depth));
// offset the heading depths by minDepth, so they always start at 1 // offset the heading depths by minDepth, so they always start at 1
const headingsToDisplay = headings const headingsToDisplay = headings
.map((h) => Object.assign({}, h, { depth: h.depth - minDepth + 1 })) .map((h) => Object.assign({}, h, { depth: h.depth - minDepth + 1 }))
.filter((headingInfo) => headingInfo.depth <= 3); .filter((headingInfo) => headingInfo.depth <= 3);
const headingsToDisplaySlugs = headingsToDisplay.map(item => item.slug); const headingsToDisplaySlugs = headingsToDisplay.map((item) => item.slug);
--- ---
<aside aria-label={"Table of Contents"}> <aside aria-label={"Table of Contents"}>
<ol <ol class={tableOfContentsStyle.tableList} role="list" id="tocList">
class={tableOfContentsStyle.tableList} {
role="list" headingsToDisplay.map((headingInfo, i) => {
id="tocList" const liClassNames = classnames(tableOfContentsStyle.tocLi, {
> [tableOfContentsStyle.tocH1]: headingInfo.depth === 1,
{headingsToDisplay.map((headingInfo, i) => { [tableOfContentsStyle.tocH2]: headingInfo.depth === 2,
const liClassNames = classnames(tableOfContentsStyle.tocLi, { [tableOfContentsStyle.tocH3]: headingInfo.depth === 3,
[tableOfContentsStyle.tocH1]: headingInfo.depth === 1, });
[tableOfContentsStyle.tocH2]: headingInfo.depth === 2, return (
[tableOfContentsStyle.tocH3]: headingInfo.depth === 3, <li class={liClassNames} data-headingitem="true">
}); <a href={`#${headingInfo.slug}`}>{headingInfo.value}</a>
return ( </li>
<li );
class={liClassNames} })
data-headingitem="true" }
> </ol>
<a href={`#${headingInfo.slug}`}>{headingInfo.value}</a>
</li>
);
})}
</ol>
</aside> </aside>
<HeadingIntersectionObserverScript headingsToDisplaySlugs={headingsToDisplaySlugs} /> <HeadingIntersectionObserverScript
headingsToDisplaySlugs={headingsToDisplaySlugs}
/>

View File

@@ -60,7 +60,9 @@
@extend %headline-uniwidth-2; @extend %headline-uniwidth-2;
} }
.tocH2, .tocH3, .tocH4 { .tocH2,
.tocH3,
.tocH4 {
a { a {
font-style: italic; font-style: italic;
} }

View File

@@ -1,39 +1,42 @@
--- ---
import styles from "./user-profile-pic.module.scss"; import styles from "./user-profile-pic.module.scss";
import { UnicornInfo } from "uu-types"; import { UnicornInfo } from "uu-types";
import {Image} from '@astrojs/image/components'; import { Image } from "@astrojs/image/components";
// TODO: Fix image loading and image 'onClick' // TODO: Fix image loading and image 'onClick'
interface UserProfilePicProps { interface UserProfilePicProps {
authors: Array<UnicornInfo>; authors: Array<UnicornInfo>;
className: string; className: string;
} }
const { authors, className } = Astro.props as UserProfilePicProps const { authors, className } = Astro.props as UserProfilePicProps;
const hasTwoAuthors = authors.length !== 1; const hasTwoAuthors = authors.length !== 1;
--- ---
<div class={`${styles.container} ${className || ""}`}>
{authors.map((unicorn, i) => {
const classesToApply = hasTwoAuthors ? styles.twoAuthor : "";
return ( <div class={`${styles.container} ${className || ""}`}>
<div {
class={`pointer ${styles.profilePicContainer} ${classesToApply}`} authors.map((unicorn, i) => {
style={`border-color: ${unicorn.color};`} const classesToApply = hasTwoAuthors ? styles.twoAuthor : "";
>
<Image return (
data-testid={`author-pic-${i}`} <div
src={unicorn.profileImgMeta.relativeServerPath} class={`pointer ${styles.profilePicContainer} ${classesToApply}`}
alt={unicorn.name} style={`border-color: ${unicorn.color};`}
sizes={"85px"} >
height={85} <Image
width={85} data-testid={`author-pic-${i}`}
format={'png'} src={unicorn.profileImgMeta.relativeServerPath}
class={`circleImg ${styles.profilePicImage} ${styles.width50} ${classesToApply}`} alt={unicorn.name}
/> sizes={"85px"}
</div> height={85}
); width={85}
})} format={"png"}
class={`circleImg ${styles.profilePicImage} ${styles.width50} ${classesToApply}`}
/>
</div>
);
})
}
</div> </div>

View File

@@ -6,32 +6,32 @@ let parent: string;
// Try & Catch to allow for hosts themselves to be passed // Try & Catch to allow for hosts themselves to be passed
// `new URL('domain.com')` will fail/throw, but is a valid host // `new URL('domain.com')` will fail/throw, but is a valid host
try { try {
const url = new URL(siteUrl); const url = new URL(siteUrl);
// URLs like 'localhost:3000' might not give host. // URLs like 'localhost:3000' might not give host.
// Throw in order to catch in wrapper handler // Throw in order to catch in wrapper handler
if (!url.host) throw new Error(); if (!url.host) throw new Error();
parent = url.host; parent = url.host;
} catch (_) { } catch (_) {
const url = new URL("https://" + siteUrl); const url = new URL("https://" + siteUrl);
parent = url.host; parent = url.host;
} }
// Twitch embed throws error with strings like 'localhost:3000', but // Twitch embed throws error with strings like 'localhost:3000', but
// those persist with `new URL().host` // those persist with `new URL().host`
if (parent.startsWith("localhost")) { if (parent.startsWith("localhost")) {
parent = "localhost"; parent = "localhost";
} }
const siteMetadata = { const siteMetadata = {
title: `Unicorn Utterances`, title: `Unicorn Utterances`,
description: `Learning programming from magically majestic words. A place to learn about all sorts of programming topics from entry-level concepts to advanced abstractions`, description: `Learning programming from magically majestic words. A place to learn about all sorts of programming topics from entry-level concepts to advanced abstractions`,
siteUrl, siteUrl,
disqusShortname: "unicorn-utterances", disqusShortname: "unicorn-utterances",
repoPath: "unicorn-utterances/unicorn-utterances", repoPath: "unicorn-utterances/unicorn-utterances",
relativeToPosts: "/content/blog", relativeToPosts: "/content/blog",
keywords: keywords:
"programming,development,mobile,web,game,utterances,software engineering,javascript,angular,react,computer science", "programming,development,mobile,web,game,utterances,software engineering,javascript,angular,react,computer science",
twitterHandle: "@unicornuttrncs", twitterHandle: "@unicornuttrncs",
}; };
export { parent, siteUrl, buildMode, siteMetadata }; export { parent, siteUrl, buildMode, siteMetadata };

View File

@@ -1,22 +1,22 @@
export default () => { export default () => {
const buildMode = process.env.BUILD_ENV || "production"; const buildMode = process.env.BUILD_ENV || "production";
let siteUrl = process.env.SITE_URL || process.env.VERCEL_URL || ""; let siteUrl = process.env.SITE_URL || process.env.VERCEL_URL || "";
if (!siteUrl) { if (!siteUrl) {
switch (buildMode) { switch (buildMode) {
case "production": case "production":
siteUrl = "https://unicorn-utterances.com"; siteUrl = "https://unicorn-utterances.com";
break; break;
case "development": case "development":
siteUrl = "http://localhost:9000"; siteUrl = "http://localhost:9000";
break; break;
default: default:
siteUrl = "https://beta.unicorn-utterances.com"; siteUrl = "https://beta.unicorn-utterances.com";
} }
} }
return { return {
buildMode, buildMode,
siteUrl, siteUrl,
}; };
}; };

View File

@@ -1,66 +1,66 @@
export const COLORS = { export const COLORS = {
//main styles //main styles
darkPrimary: { light: "#153E67", dark: "#E4F4FF" }, darkPrimary: { light: "#153E67", dark: "#E4F4FF" },
primary: { light: "#127DB3", dark: "#127DB3" }, primary: { light: "#127DB3", dark: "#127DB3" },
lightPrimary: { light: "#315e81", dark: "#bdd9e9" }, lightPrimary: { light: "#315e81", dark: "#bdd9e9" },
black: { light: "black", dark: "white" }, black: { light: "black", dark: "white" },
white: { light: "white", dark: "black" }, white: { light: "white", dark: "black" },
darkGrey: { light: "rgba(0, 0, 0, 0.64)", dark: "rgba(255, 255, 255, .64)" }, darkGrey: { light: "rgba(0, 0, 0, 0.64)", dark: "rgba(255, 255, 255, .64)" },
highImpactBlack: { highImpactBlack: {
light: "rgba(0, 0, 0, 0.87)", light: "rgba(0, 0, 0, 0.87)",
dark: "rgba(255, 255, 255, .87)", dark: "rgba(255, 255, 255, .87)",
}, },
midImpactBlack: { midImpactBlack: {
light: "rgba(0, 0, 0, 0.64)", light: "rgba(0, 0, 0, 0.64)",
dark: "rgba(255, 255, 255, .64)", dark: "rgba(255, 255, 255, .64)",
}, },
lowImpactBlack: { lowImpactBlack: {
light: "rgba(0, 0, 0, 0.58)", light: "rgba(0, 0, 0, 0.58)",
dark: "rgba(255, 255, 255, .58)", dark: "rgba(255, 255, 255, .58)",
}, },
minImpactBlack: { minImpactBlack: {
light: "rgba(0, 0, 0, 0.2)", light: "rgba(0, 0, 0, 0.2)",
dark: "rgba(255, 255, 255, .2)", dark: "rgba(255, 255, 255, .2)",
}, },
backgroundColor: { light: "#E4F4FF", dark: "#072a41" }, backgroundColor: { light: "#E4F4FF", dark: "#072a41" },
cardActiveBackground: { light: "#EBF6FC", dark: "#163954" }, cardActiveBackground: { light: "#EBF6FC", dark: "#163954" },
cardActiveBoxShadow: { cardActiveBoxShadow: {
light: "0px 2px 4px rgba(11, 37, 104, 0.27), inset 0px 1px 0px #FFFFFF", light: "0px 2px 4px rgba(11, 37, 104, 0.27), inset 0px 1px 0px #FFFFFF",
dark: "0px 2px 4px rgba(0, 0, 0, 0.27), inset 0px 1px 0px #435e75", dark: "0px 2px 4px rgba(0, 0, 0, 0.27), inset 0px 1px 0px #435e75",
}, },
codeBlockBackground: { light: "white", dark: "#202746" }, codeBlockBackground: { light: "white", dark: "#202746" },
codeInlineBackground: { light: "#cbe8fb", dark: "#1d495e" }, codeInlineBackground: { light: "#cbe8fb", dark: "#1d495e" },
//code styles //code styles
codeBackgroundColor: { light: "#fff", dark: "#161b1d" }, codeBackgroundColor: { light: "#fff", dark: "#161b1d" },
textColor: { light: "#5e6687", dark: "#7ea2b4" }, textColor: { light: "#5e6687", dark: "#7ea2b4" },
stringColor: { light: "#007396", dark: "#7ee2c4" }, stringColor: { light: "#007396", dark: "#7ee2c4" },
keywordColor: { light: "#846c00", dark: "#b5ea94" }, keywordColor: { light: "#846c00", dark: "#b5ea94" },
operatorColor: { light: "#b74c00", dark: "#935c25" }, operatorColor: { light: "#b74c00", dark: "#935c25" },
punctuationColor: { light: "#006fce", dark: "#7ea2b4" }, punctuationColor: { light: "#006fce", dark: "#7ea2b4" },
constantColor: { light: "#aa05d4", dark: "#cf8ae1" }, constantColor: { light: "#aa05d4", dark: "#cf8ae1" },
functionColor: { light: "#5357d2", dark: "#c1c3ff" }, functionColor: { light: "#5357d2", dark: "#c1c3ff" },
selectionColor: { light: "#dfe2f1", dark: "#cfe0ec" }, selectionColor: { light: "#dfe2f1", dark: "#cfe0ec" },
commentColor: { light: "#898ea4", dark: "#c7d5d7" }, commentColor: { light: "#898ea4", dark: "#c7d5d7" },
propColor: { light: "#c08b30", dark: "#8a8a0f" }, propColor: { light: "#c08b30", dark: "#8a8a0f" },
varColor: { light: "#3d8fd1", dark: "#88c1e2" }, varColor: { light: "#3d8fd1", dark: "#88c1e2" },
selectorColor: { light: "#6679cc", dark: "#bdbdff" }, selectorColor: { light: "#6679cc", dark: "#bdbdff" },
urlColor: { light: "#22a9c9", dark: "#9affde" }, urlColor: { light: "#22a9c9", dark: "#9affde" },
insertedUnderlineColor: { light: "#202746", dark: "#ebf8ff" }, insertedUnderlineColor: { light: "#202746", dark: "#ebf8ff" },
highlightColor: { light: "#c94922", dark: "#ff92c0" }, highlightColor: { light: "#c94922", dark: "#ff92c0" },
lineNumbersColor: { light: "#979db4", dark: "#abe1fa" }, lineNumbersColor: { light: "#979db4", dark: "#abe1fa" },
lineHighlightColor: { lineHighlightColor: {
light: "rgba(107, 115, 148, 0.2)", light: "rgba(107, 115, 148, 0.2)",
dark: "rgba(235, 248, 255, 0.2)", dark: "rgba(235, 248, 255, 0.2)",
}, },
lineHighlightFadeColor: { lineHighlightFadeColor: {
light: "rgba(107, 115, 148, 0)", light: "rgba(107, 115, 148, 0)",
dark: "rgba(235, 248, 255, 0)", dark: "rgba(235, 248, 255, 0)",
}, },
scrollBarBG: { scrollBarBG: {
light: "rgba(18, 125, 179, 0.3)", light: "rgba(18, 125, 179, 0.3)",
dark: "rgba(228, 244, 255, 0.3)", dark: "rgba(228, 244, 255, 0.3)",
}, },
scrollBarThumb: { light: "var(--primary)", dark: "var(--darkPrimary)" }, scrollBarThumb: { light: "var(--primary)", dark: "var(--darkPrimary)" },
}; };
export const COLOR_MODE_STORAGE_KEY = "currentTheme"; export const COLOR_MODE_STORAGE_KEY = "currentTheme";

File diff suppressed because one or more lines are too long

View File

@@ -7,421 +7,421 @@
@import "./convertkit"; @import "./convertkit";
:root { :root {
--animSpeed: 200ms; --animSpeed: 200ms;
--animStyle: ease-in-out; --animStyle: ease-in-out;
--cardOutlineStyle: 1px solid var(--primary); --cardOutlineStyle: 1px solid var(--primary);
--cardRadius: #{$baseUnit}px; --cardRadius: #{$baseUnit}px;
--filterBarIconSize: #{3 * $baseUnit}px; --filterBarIconSize: #{3 * $baseUnit}px;
--listViewPadding: #{1.5 * $baseUnit}px; --listViewPadding: #{1.5 * $baseUnit}px;
font-size: $rootFontSize; font-size: $rootFontSize;
line-height: 1.2; line-height: 1.2;
scrollbar-color: var(--darkPrimary) var(--backgroundColor); scrollbar-color: var(--darkPrimary) var(--backgroundColor);
transition: scrollbar-color var(--animStyle) var(--animSpeed); transition: scrollbar-color var(--animStyle) var(--animSpeed);
} }
@include from($startMediumScreenSize) { @include from($startMediumScreenSize) {
:root { :root {
--listViewPadding: #{$baseUnit * 2.5}px; --listViewPadding: #{$baseUnit * 2.5}px;
} }
} }
:focus { :focus {
outline-color: var(--darkPrimary); outline-color: var(--darkPrimary);
} }
//without this all <button>s have bad outline colors in firefox //without this all <button>s have bad outline colors in firefox
:focus::-moz-focus-inner { :focus::-moz-focus-inner {
padding: 0; //prevent weirdness just in case padding: 0; //prevent weirdness just in case
border-color: var(--darkPrimary); border-color: var(--darkPrimary);
} }
*::-webkit-scrollbar { *::-webkit-scrollbar {
width: 12px; width: 12px;
} }
*::-webkit-scrollbar-track { *::-webkit-scrollbar-track {
background: var(--scrollBarBG); background: var(--scrollBarBG);
border-radius: 10px; border-radius: 10px;
} }
*::-webkit-scrollbar-thumb { *::-webkit-scrollbar-thumb {
border-radius: 10px; border-radius: 10px;
background: var(--scrollBarThumb); background: var(--scrollBarThumb);
} }
.listViewContent { .listViewContent {
margin: 0 auto; margin: 0 auto;
max-width: #{160 * $baseUnit}px; // 1280px max-width: #{160 * $baseUnit}px; // 1280px
padding: 0 var(--listViewPadding); padding: 0 var(--listViewPadding);
} }
.postViewContent { .postViewContent {
padding: #{$baseUnit * 2.5}px; padding: #{$baseUnit * 2.5}px;
overflow-wrap: break-word; overflow-wrap: break-word;
} }
.postViewContent > * { .postViewContent > * {
margin: 0 auto; margin: 0 auto;
} }
body { body {
background-color: var(--backgroundColor); background-color: var(--backgroundColor);
margin: 0; margin: 0;
padding: 0; padding: 0;
@extend %body-1; @extend %body-1;
color: var(--black); color: var(--black);
transition: color var(--animStyle) var(--animSpeed), transition: color var(--animStyle) var(--animSpeed),
background-color var(--animStyle) var(--animSpeed); background-color var(--animStyle) var(--animSpeed);
} }
.medium-zoom-overlay { .medium-zoom-overlay {
background: var(--backgroundColor) !important; background: var(--backgroundColor) !important;
} }
/* https://snook.ca/archives/html_and_css/hiding-content-for-accessibility */ /* https://snook.ca/archives/html_and_css/hiding-content-for-accessibility */
.visually-hidden { .visually-hidden {
position: absolute !important; position: absolute !important;
height: 1px; height: 1px;
width: 1px; width: 1px;
overflow: hidden; overflow: hidden;
clip: rect(1px 1px 1px 1px); /* IE6, IE7 */ clip: rect(1px 1px 1px 1px); /* IE6, IE7 */
clip: rect(1px, 1px, 1px, 1px); clip: rect(1px, 1px, 1px, 1px);
} }
.visually-hidden a:focus, .visually-hidden a:focus,
.visually-hidden input:focus, .visually-hidden input:focus,
.visually-hidden button:focus { .visually-hidden button:focus {
position: static; position: static;
width: auto; width: auto;
height: auto; height: auto;
} }
.filterDropdown { .filterDropdown {
// This maps to the dropdown button. If the asset is changes, thus this must as well // This maps to the dropdown button. If the asset is changes, thus this must as well
transform-origin: 26px 26px; transform-origin: 26px 26px;
transition: transform 300ms var(--animStyle); transition: transform 300ms var(--animStyle);
} }
.expandedIcon .filterDropdown { .expandedIcon .filterDropdown {
transform: rotate(180deg); transform: rotate(180deg);
} }
.baseBtn { .baseBtn {
cursor: pointer; cursor: pointer;
} }
.baseBtn, .baseBtn,
.btnLike { .btnLike {
appearance: none; appearance: none;
text-decoration: none; text-decoration: none;
background: none; background: none;
border: none; border: none;
transition: background var(--animSpeed) var(--animStyle), transition: background var(--animSpeed) var(--animStyle),
box-shadow var(--animSpeed) var(--animStyle), box-shadow var(--animSpeed) var(--animStyle),
border-color var(--animSpeed) var(--animStyle), border-color var(--animSpeed) var(--animStyle),
color var(--animStyle) var(--animSpeed); color var(--animStyle) var(--animSpeed);
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
flex-shrink: 0; flex-shrink: 0;
border-radius: var(--cardRadius); border-radius: var(--cardRadius);
font-size: 1rem; font-size: 1rem;
padding: #{math.div($baseUnit, 2)}px #{$baseUnit}px; padding: #{math.div($baseUnit, 2)}px #{$baseUnit}px;
color: var(--darkPrimary); color: var(--darkPrimary);
} }
.baseBtn:hover { .baseBtn:hover {
background: var(--cardActiveBackground); background: var(--cardActiveBackground);
border-color: transparent; border-color: transparent;
box-shadow: var(--cardActiveBoxShadow); box-shadow: var(--cardActiveBoxShadow);
} }
.baseBtn svg, .baseBtn svg,
.btnLike svg { .btnLike svg {
$size: #{$baseUnit * 4}px; $size: #{$baseUnit * 4}px;
height: $size; height: $size;
width: $size; width: $size;
flex-shrink: 0; flex-shrink: 0;
} }
$pendIconMarg: #{$baseUnit}px; $pendIconMarg: #{$baseUnit}px;
.baseBtn.prependIcon svg, .baseBtn.prependIcon svg,
.btnLike.prependIcon svg { .btnLike.prependIcon svg {
margin-right: $pendIconMarg; margin-right: $pendIconMarg;
} }
.baseBtn.appendIcon svg, .baseBtn.appendIcon svg,
.btnLike.appendIcon svg { .btnLike.appendIcon svg {
margin-left: $pendIconMarg; margin-left: $pendIconMarg;
} }
.post-body { .post-body {
margin: 0 auto #{$baseUnit * 4}px; margin: 0 auto #{$baseUnit * 4}px;
max-width: 768px; max-width: 768px;
line-height: 1.7; line-height: 1.7;
// Fix autolink-headings anchors positioning // Fix autolink-headings anchors positioning
.anchor { .anchor {
line-height: 1; line-height: 1;
padding-right: 24px; padding-right: 24px;
svg { svg {
vertical-align: middle; vertical-align: middle;
} }
} }
.anchor.before { .anchor.before {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
transform: translateX(-100%); transform: translateX(-100%);
padding-right: 24px; padding-right: 24px;
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
} }
h1 .anchor svg, h1 .anchor svg,
h2 .anchor svg, h2 .anchor svg,
h3 .anchor svg, h3 .anchor svg,
h4 .anchor svg, h4 .anchor svg,
h5 .anchor svg, h5 .anchor svg,
h6 .anchor svg { h6 .anchor svg {
visibility: hidden; visibility: hidden;
} }
h1:hover .anchor svg, h1:hover .anchor svg,
h2:hover .anchor svg, h2:hover .anchor svg,
h3:hover .anchor svg, h3:hover .anchor svg,
h4:hover .anchor svg, h4:hover .anchor svg,
h5:hover .anchor svg, h5:hover .anchor svg,
h6:hover .anchor svg, h6:hover .anchor svg,
h1 .anchor:focus svg, h1 .anchor:focus svg,
h2 .anchor:focus svg, h2 .anchor:focus svg,
h3 .anchor:focus svg, h3 .anchor:focus svg,
h4 .anchor:focus svg, h4 .anchor:focus svg,
h5 .anchor:focus svg, h5 .anchor:focus svg,
h6 .anchor:focus svg { h6 .anchor:focus svg {
visibility: visible; visibility: visible;
} }
img { img {
margin: 0 auto; margin: 0 auto;
display: block; display: block;
max-width: 100%; max-width: 100%;
&[src$=".svg"] { &[src$=".svg"] {
width: 100%; width: 100%;
max-height: 50vh; max-height: 50vh;
} }
} }
h1, h1,
h2, h2,
h3, h3,
h4, h4,
h5, h5,
h6 { h6 {
margin-top: 1.25em; margin-top: 1.25em;
line-height: 1.5; line-height: 1.5;
margin-bottom: 0; margin-bottom: 0;
} }
h1 + h2, h1 + h2,
h2 + h3, h2 + h3,
h3 + h4, h3 + h4,
h4 + h5, h4 + h5,
h5 + h6 { h5 + h6 {
margin-top: 0.75em; margin-top: 0.75em;
} }
table tr:last-child th:first-child { table tr:last-child th:first-child {
border-bottom-left-radius: 10px; border-bottom-left-radius: 10px;
} }
table tr:last-child td:first-child { table tr:last-child td:first-child {
border-bottom-left-radius: 10px; border-bottom-left-radius: 10px;
} }
table tr:last-child td:last-child { table tr:last-child td:last-child {
border-bottom-right-radius: 10px; border-bottom-right-radius: 10px;
} }
.table-container { .table-container {
max-width: 100%; max-width: 100%;
overflow: auto; overflow: auto;
} }
table { table {
border: var(--cardOutlineStyle); border: var(--cardOutlineStyle);
border-radius: var(--cardRadius); border-radius: var(--cardRadius);
border-collapse: collapse; border-collapse: collapse;
// Border-collapse and border-radius don't mix. This is a workaround for that issue // Border-collapse and border-radius don't mix. This is a workaround for that issue
box-shadow: 0 0 0 1px var(--primary); box-shadow: 0 0 0 1px var(--primary);
overflow: hidden; overflow: hidden;
@include until($endSmallScreenSize) { @include until($endSmallScreenSize) {
ul { ul {
padding: 0; padding: 0;
list-style: none; list-style: none;
} }
} }
} }
tr, tr,
td, td,
th { th {
border: var(--cardOutlineStyle); border: var(--cardOutlineStyle);
} }
td, td,
th { th {
padding-left: 1rem; padding-left: 1rem;
padding-right: 1rem; padding-right: 1rem;
@include until($endSmallScreenSize) { @include until($endSmallScreenSize) {
padding-left: 5px; padding-left: 5px;
padding-right: 5px; padding-right: 5px;
} }
} }
iframe { iframe {
width: 100%; width: 100%;
min-height: 500px; min-height: 500px;
border: var(--cardOutlineStyle); border: var(--cardOutlineStyle);
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
} }
p > code { p > code {
display: inline; display: inline;
padding: 0 0.4em; padding: 0 0.4em;
font-size: 85%; font-size: 85%;
color: var(--black); color: var(--black);
background-color: var(--codeInlineBackground); background-color: var(--codeInlineBackground);
border-radius: 4px; border-radius: 4px;
} }
details { details {
border: 2px solid var(--darkPrimary); border: 2px solid var(--darkPrimary);
border-radius: 0.5rem; border-radius: 0.5rem;
margin-bottom: 1em; margin-bottom: 1em;
summary { summary {
padding: .5rem 1.5rem; padding: 0.5rem 1.5rem;
background: var(--darkPrimary); background: var(--darkPrimary);
color: var(--backgroundColor); color: var(--backgroundColor);
cursor: pointer; cursor: pointer;
} }
summary ~ * { summary ~ * {
margin-left: 1.5rem; margin-left: 1.5rem;
margin-right: 1.5rem; margin-right: 1.5rem;
} }
} }
} }
.post-lower-area { .post-lower-area {
margin: 0 auto; margin: 0 auto;
max-width: #{$baseUnit * 115}px; max-width: #{$baseUnit * 115}px;
.postBottom { .postBottom {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@include until($startMediumScreenSize) { @include until($startMediumScreenSize) {
flex-direction: column; flex-direction: column;
.btnLike { .btnLike {
order: 2; order: 2;
} }
.baseBtn { .baseBtn {
margin-top: 20px; margin-top: 20px;
margin-bottom: 5px; margin-bottom: 5px;
} }
} }
} }
} }
// Please use this sparingly. There's massive A11y concerns // Please use this sparingly. There's massive A11y concerns
.unlink { .unlink {
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
} }
pre { pre {
overflow: auto; overflow: auto;
border: var(--cardOutlineStyle); border: var(--cardOutlineStyle);
border-radius: 8px; border-radius: 8px;
background: white; background: white;
} }
.lowercase { .lowercase {
text-transform: lowercase; text-transform: lowercase;
} }
.pointer { .pointer {
cursor: pointer; cursor: pointer;
} }
.circleImg { .circleImg {
flex-shrink: 0; flex-shrink: 0;
flex-grow: 0; flex-grow: 0;
border-radius: 50%; border-radius: 50%;
} }
a { a {
color: var(--darkPrimary); color: var(--darkPrimary);
} }
svg.strokeicon { svg.strokeicon {
&, &,
* { * {
transition: stroke var(--animStyle) var(--animSpeed); transition: stroke var(--animStyle) var(--animSpeed);
stroke: var(--darkPrimary); stroke: var(--darkPrimary);
} }
} }
svg:not(.strokeicon) { svg:not(.strokeicon) {
&, &,
* { * {
transition: fill var(--animStyle) var(--animSpeed); transition: fill var(--animStyle) var(--animSpeed);
fill: var(--darkPrimary); fill: var(--darkPrimary);
} }
} }
.marginZeroAutoChild { .marginZeroAutoChild {
& > * { & > * {
margin: 0 auto; margin: 0 auto;
} }
} }
li > ul > li { li > ul > li {
margin: 1rem 0; margin: 1rem 0;
} }
.toc-is-active { .toc-is-active {
font-weight: bold; font-weight: bold;
a { a {
color: var(--darkPrimary) !important; color: var(--darkPrimary) !important;
} }
} }
kbd { kbd {
background-color: #eee; background-color: #eee;
border-radius: 3px; border-radius: 3px;
border: 1px solid #b4b4b4; border: 1px solid #b4b4b4;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
0 2px 0 0 rgba(255, 255, 255, 0.7) inset; 0 2px 0 0 rgba(255, 255, 255, 0.7) inset;
color: #333; color: #333;
display: inline-block; display: inline-block;
font-size: 0.85em; font-size: 0.85em;
font-weight: 700; font-weight: 700;
line-height: 1; line-height: 1;
padding: 2px 4px; padding: 2px 4px;
white-space: nowrap; white-space: nowrap;
} }

View File

@@ -1,29 +1,28 @@
--- ---
import {v4 as uuidV4} from 'uuid'; import { v4 as uuidV4 } from "uuid";
const id = uuidV4(); const id = uuidV4();
const clipId = `path-1-inside-1${id}`; const clipId = `path-1-inside-1${id}`;
const props = Astro.props; const props = Astro.props;
--- ---
<svg <svg
width="36" width="36"
height="36" height="36"
viewBox="0 0 36 36" viewBox="0 0 36 36"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
{...props} {...props}
> >
<g clip-path={`url(#${clipId})`}> <g clip-path={`url(#${clipId})`}>
<path <path
fill-rule="evenodd" fill-rule="evenodd"
clip-rule="evenodd" clip-rule="evenodd"
d="M24.6893 7.31065C23.0659 5.6873 20.4339 5.6873 18.8106 7.31066L8.06059 18.0607C5.33267 20.7886 5.33267 25.2114 8.06059 27.9393C10.7885 30.6673 15.2113 30.6673 17.9393 27.9393L30.4393 15.4393C31.0251 14.8535 31.9748 14.8535 32.5606 15.4393C33.1464 16.0251 33.1464 16.9749 32.5606 17.5607L20.0606 30.0607C16.1611 33.9602 9.83876 33.9601 5.93927 30.0607C2.03978 26.1612 2.03977 19.8388 5.93927 15.9393L16.6893 5.18934C19.4842 2.39441 24.0157 2.39441 26.8106 5.18933C29.6055 7.98426 29.6055 12.5157 26.8106 15.3107L16.0606 26.0606C14.3702 27.751 11.6296 27.751 9.93927 26.0607C8.24891 24.3703 8.24891 21.6297 9.93927 19.9393L19.9393 9.93933C20.5251 9.35355 21.4748 9.35355 22.0606 9.93933C22.6464 10.5251 22.6464 11.4749 22.0606 12.0607L12.0606 22.0607C11.5418 22.5794 11.5418 23.4205 12.0606 23.9393C12.5794 24.4581 13.4205 24.4581 13.9393 23.9393L24.6893 13.1893C26.3126 11.566 26.3126 8.93401 24.6893 7.31065Z" d="M24.6893 7.31065C23.0659 5.6873 20.4339 5.6873 18.8106 7.31066L8.06059 18.0607C5.33267 20.7886 5.33267 25.2114 8.06059 27.9393C10.7885 30.6673 15.2113 30.6673 17.9393 27.9393L30.4393 15.4393C31.0251 14.8535 31.9748 14.8535 32.5606 15.4393C33.1464 16.0251 33.1464 16.9749 32.5606 17.5607L20.0606 30.0607C16.1611 33.9602 9.83876 33.9601 5.93927 30.0607C2.03978 26.1612 2.03977 19.8388 5.93927 15.9393L16.6893 5.18934C19.4842 2.39441 24.0157 2.39441 26.8106 5.18933C29.6055 7.98426 29.6055 12.5157 26.8106 15.3107L16.0606 26.0606C14.3702 27.751 11.6296 27.751 9.93927 26.0607C8.24891 24.3703 8.24891 21.6297 9.93927 19.9393L19.9393 9.93933C20.5251 9.35355 21.4748 9.35355 22.0606 9.93933C22.6464 10.5251 22.6464 11.4749 22.0606 12.0607L12.0606 22.0607C11.5418 22.5794 11.5418 23.4205 12.0606 23.9393C12.5794 24.4581 13.4205 24.4581 13.9393 23.9393L24.6893 13.1893C26.3126 11.566 26.3126 8.93401 24.6893 7.31065Z"
fill="#153E67" fill="#153E67"></path>
/> </g>
</g> <defs>
<defs> <clip-path id={clipId}>
<clipPath id={clipId}> <rect width="36" height="36" fill="white"></rect>
<rect width="36" height="36" fill="white" /> </clip-path>
</clipPath> </defs>
</defs> </svg>
</svg>

View File

@@ -1,27 +1,26 @@
--- ---
import {v4 as uuidV4} from 'uuid'; import { v4 as uuidV4 } from "uuid";
const id = uuidV4(); const id = uuidV4();
const clipId = `path-1-inside-1${id}`; const clipId = `path-1-inside-1${id}`;
const props = Astro.props; const props = Astro.props;
--- ---
<svg <svg
width="36" width="36"
height="36" height="36"
viewBox="0 0 36 36" viewBox="0 0 36 36"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
{...props} {...props}
> >
<g clipPath={`url(#${clipId.current})`}> <g clip-path={`url(#${clipId})`}>
<path <path
d="M1.43934 32.4393C0.853553 33.0251 0.853553 33.9749 1.43934 34.5607C2.02513 35.1464 2.97487 35.1464 3.56066 34.5607L1.43934 32.4393ZM24.0607 14.0607C24.6464 13.4749 24.6464 12.5251 24.0607 11.9393C23.4749 11.3536 22.5251 11.3536 21.9393 11.9393L24.0607 14.0607ZM8.67157 14.3284L7.61091 13.2678L8.67157 14.3284ZM14.5607 23.5607L24.0607 14.0607L21.9393 11.9393L12.4393 21.4393L14.5607 23.5607ZM26.5 21H13.5V24H26.5V21ZM3.56066 34.5607L8.56066 29.5607L6.43934 27.4393L1.43934 32.4393L3.56066 34.5607ZM8.56066 29.5607L14.5607 23.5607L12.4393 21.4393L6.43934 27.4393L8.56066 29.5607ZM7.5 30H18.8431V27H7.5V30ZM22.7322 28.3891L31.5607 19.5607L29.4393 17.4393L20.6109 26.2678L22.7322 28.3891ZM9 28.5V17.1569H6V28.5H9ZM9.73223 15.3891L18.5607 6.56066L16.4393 4.43934L7.61091 13.2678L9.73223 15.3891ZM18.5607 6.56066C21.5647 3.55659 26.4353 3.5566 29.4393 6.56066L31.5607 4.43934C27.385 0.263705 20.615 0.263698 16.4393 4.43934L18.5607 6.56066ZM9 17.1569C9 16.4938 9.26339 15.8579 9.73223 15.3891L7.61091 13.2678C6.57946 14.2992 6 15.6982 6 17.1569H9ZM31.5607 19.5607C35.7363 15.385 35.7363 8.61497 31.5607 4.43934L29.4393 6.56066C32.4434 9.56472 32.4434 14.4353 29.4393 17.4393L31.5607 19.5607ZM18.8431 30C20.3018 30 21.7008 29.4205 22.7322 28.3891L20.6109 26.2678C20.1421 26.7366 19.5062 27 18.8431 27V30Z" d="M1.43934 32.4393C0.853553 33.0251 0.853553 33.9749 1.43934 34.5607C2.02513 35.1464 2.97487 35.1464 3.56066 34.5607L1.43934 32.4393ZM24.0607 14.0607C24.6464 13.4749 24.6464 12.5251 24.0607 11.9393C23.4749 11.3536 22.5251 11.3536 21.9393 11.9393L24.0607 14.0607ZM8.67157 14.3284L7.61091 13.2678L8.67157 14.3284ZM14.5607 23.5607L24.0607 14.0607L21.9393 11.9393L12.4393 21.4393L14.5607 23.5607ZM26.5 21H13.5V24H26.5V21ZM3.56066 34.5607L8.56066 29.5607L6.43934 27.4393L1.43934 32.4393L3.56066 34.5607ZM8.56066 29.5607L14.5607 23.5607L12.4393 21.4393L6.43934 27.4393L8.56066 29.5607ZM7.5 30H18.8431V27H7.5V30ZM22.7322 28.3891L31.5607 19.5607L29.4393 17.4393L20.6109 26.2678L22.7322 28.3891ZM9 28.5V17.1569H6V28.5H9ZM9.73223 15.3891L18.5607 6.56066L16.4393 4.43934L7.61091 13.2678L9.73223 15.3891ZM18.5607 6.56066C21.5647 3.55659 26.4353 3.5566 29.4393 6.56066L31.5607 4.43934C27.385 0.263705 20.615 0.263698 16.4393 4.43934L18.5607 6.56066ZM9 17.1569C9 16.4938 9.26339 15.8579 9.73223 15.3891L7.61091 13.2678C6.57946 14.2992 6 15.6982 6 17.1569H9ZM31.5607 19.5607C35.7363 15.385 35.7363 8.61497 31.5607 4.43934L29.4393 6.56066C32.4434 9.56472 32.4434 14.4353 29.4393 17.4393L31.5607 19.5607ZM18.8431 30C20.3018 30 21.7008 29.4205 22.7322 28.3891L20.6109 26.2678C20.1421 26.7366 19.5062 27 18.8431 27V30Z"
fill="#153E67" fill="#153E67"></path>
/> </g>
</g> <defs>
<defs> <clip-path id={clipId}>
<clipPath id={clipId.current}> <rect width="36" height="36" fill="white"></rect>
<rect width="36" height="36" fill="white" /> </clip-path>
</clipPath> </defs>
</defs>
</svg> </svg>

View File

@@ -2,69 +2,69 @@
import ThemeStyle from "../page-components/layouts/theme-style.astro"; import ThemeStyle from "../page-components/layouts/theme-style.astro";
import BlockingThemeChangerScript from "../page-components/layouts/blocking-theme-changer-script.astro"; import BlockingThemeChangerScript from "../page-components/layouts/blocking-theme-changer-script.astro";
import Layout from "../components/layout/layout.astro"; import Layout from "../components/layout/layout.astro";
import '../global.scss'; import "../global.scss";
--- ---
<html class="light"> <html class="light">
<head> <head>
<meta name="viewport" content="width=device-width"> <meta name="viewport" content="width=device-width" />
<meta charset="utf-8"/> <meta charset="utf-8" />
<link <link
rel="preload" rel="preload"
as="style" as="style"
href="https://fonts.googleapis.com/css?family=Asap:400,700%7CWork+Sans:400,500,600,700%7CArchivo:400,500,600%7CRoboto+Mono:400&display=swap" href="https://fonts.googleapis.com/css?family=Asap:400,700%7CWork+Sans:400,500,600,700%7CArchivo:400,500,600%7CRoboto+Mono:400&display=swap"
/> />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<link rel="manifest" href="/manifest.json" /> <link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#127db3" /> <meta name="theme-color" content="#127db3" />
<link <link
rel="apple-touch-icon" rel="apple-touch-icon"
sizes="48x48" sizes="48x48"
href="/icons/icon-48x48.png?v=2a" href="/icons/icon-48x48.png?v=2a"
/> />
<link <link
rel="apple-touch-icon" rel="apple-touch-icon"
sizes="72x72" sizes="72x72"
href="/icons/icon-72x72.png?v=2a" href="/icons/icon-72x72.png?v=2a"
/> />
<link <link
rel="apple-touch-icon" rel="apple-touch-icon"
sizes="96x96" sizes="96x96"
href="/icons/icon-96x96.png?v=2a" href="/icons/icon-96x96.png?v=2a"
/> />
<link <link
rel="apple-touch-icon" rel="apple-touch-icon"
sizes="144x144" sizes="144x144"
href="/icons/icon-144x144.png?v=2a" href="/icons/icon-144x144.png?v=2a"
/> />
<link <link
rel="apple-touch-icon" rel="apple-touch-icon"
sizes="192x192" sizes="192x192"
href="/icons/icon-192x192.png?v=2a" href="/icons/icon-192x192.png?v=2a"
/> />
<link <link
rel="apple-touch-icon" rel="apple-touch-icon"
sizes="256x256" sizes="256x256"
href="/icons/icon-256x256.png?v=2a" href="/icons/icon-256x256.png?v=2a"
/> />
<link <link
rel="apple-touch-icon" rel="apple-touch-icon"
sizes="384x384" sizes="384x384"
href="/icons/icon-384x384.png?v=2a" href="/icons/icon-384x384.png?v=2a"
/> />
<link <link
rel="apple-touch-icon" rel="apple-touch-icon"
sizes="512x512" sizes="512x512"
href="/icons/icon-512x512.png?v=2a" href="/icons/icon-512x512.png?v=2a"
/> />
<script defer src="/uninstall-sw.js" is:inline/> <script defer src="/uninstall-sw.js" is:inline></script>
<ThemeStyle/> <ThemeStyle />
<slot name="head"/> <slot name="head" />
</head> </head>
<body> <body>
<BlockingThemeChangerScript/> <BlockingThemeChangerScript />
<Layout> <Layout>
<slot /> <slot />
</Layout> </Layout>
</body> </body>
</html> </html>

View File

@@ -5,7 +5,7 @@ import dayjs from "dayjs";
import { PostInfo } from "types/PostInfo"; import { PostInfo } from "types/PostInfo";
interface PostMetadataProps { interface PostMetadataProps {
post: PostInfo; post: PostInfo;
} }
const { post } = Astro.props as PostMetadataProps; const { post } = Astro.props as PostMetadataProps;
@@ -14,8 +14,8 @@ const { authorsMeta } = post;
let originalHost; let originalHost;
if (post.originalLink) { if (post.originalLink) {
const url = new URL(post.originalLink); const url = new URL(post.originalLink);
originalHost = url.host; originalHost = url.host;
} }
const publishedStr = dayjs(post.published).format("MMMM D, YYYY"); const publishedStr = dayjs(post.published).format("MMMM D, YYYY");
@@ -24,32 +24,43 @@ const publishedStr = dayjs(post.published).format("MMMM D, YYYY");
--- ---
<div class={styles.container}> <div class={styles.container}>
<UserProfilePic authors={authorsMeta} class={styles.postMetadataAuthorImagesContainer} /> <UserProfilePic
<div class={styles.textDiv}> authors={authorsMeta}
<h2 class={styles.authorName} data-testid="post-meta-author-name"> class={styles.postMetadataAuthorImagesContainer}
<span>by </span> />
{authorsMeta.map((author, i) => { <div class={styles.textDiv}>
return ( <h2 class={styles.authorName} data-testid="post-meta-author-name">
<> <span>by</span>
{i !== 0 && <span>{", "}</span>} {
<a href={`/unicorns/${author.id}`} class={styles.authorLink}> authorsMeta.map((author, i) => {
{author.name} return (
</a> <>
</> {i !== 0 && <span>{", "}</span>}
); <a href={`/unicorns/${author.id}`} class={styles.authorLink}>
})} {author.name}
</h2> </a>
<div class={styles.belowName}> </>
<p>{publishedStr}</p> );
<p>{post.wordCount} words</p> })
</div> }
</div> </h2>
{!!post.originalLink && ( <div class={styles.belowName}>
<p class={styles.originalLink}> <p>{publishedStr}</p>
Originally posted at&nbsp; <p>{post.wordCount} words</p>
<a href={post.originalLink} target="_blank" rel="nofollow noopener noreferrer"> </div>
{originalHost} </div>
</a> {
</p> !!post.originalLink && (
)} <p class={styles.originalLink}>
Originally posted at&nbsp;
<a
href={post.originalLink}
target="_blank"
rel="nofollow noopener noreferrer"
>
{originalHost}
</a>
</p>
)
}
</div> </div>

View File

@@ -3,19 +3,15 @@ import styles from "./post-title-header.module.scss";
import { PostInfo } from "types/index"; import { PostInfo } from "types/index";
interface PostTitleHeaderProps { interface PostTitleHeaderProps {
post: PostInfo; post: PostInfo;
} }
const {post} = Astro.props as PostTitleHeaderProps; const { post } = Astro.props as PostTitleHeaderProps;
const { title, tags } = post; const { title, tags } = post;
--- ---
<div class={styles.container}> <div class={styles.container}>
<h1 class={styles.title}>{title}</h1> <h1 class={styles.title}>{title}</h1>
<ul aria-label="Post tags" role="list" class={styles.tags}> <ul aria-label="Post tags" role="list" class={styles.tags}>
{tags.map((tag) => ( {tags.map((tag) => <li role="listitem">{tag}</li>)}
<li role="listitem"> </ul>
{tag}
</li>
))}
</ul>
</div> </div>

View File

@@ -3,44 +3,43 @@ import suggestedStyle from "./suggested-articles.module.scss";
import { Languages, PostInfo } from "types/index"; import { Languages, PostInfo } from "types/index";
interface TableOfContentsProps { interface TableOfContentsProps {
suggestedArticles: PostInfo[]; suggestedArticles: PostInfo[];
lang?: Languages; lang?: Languages;
} }
const { const { suggestedArticles, lang } = Astro.props as TableOfContentsProps;
suggestedArticles,
lang,
} = Astro.props as TableOfContentsProps;
--- ---
{!suggestedArticles.length ? null : <aside aria-label={"Suggested Articles"}> {
<ol role={"list"} class={suggestedStyle.list}> !suggestedArticles.length ? null : (
<h2 class={suggestedStyle.header}>Related posts</h2> <aside aria-label={"Suggested Articles"}>
{suggestedArticles.map((suggestedArticle, i) => { <ol role={"list"} class={suggestedStyle.list}>
const authorNames = suggestedArticle.authorsMeta <h2 class={suggestedStyle.header}>Related posts</h2>
.map((author) => author.name) {suggestedArticles.map((suggestedArticle, i) => {
.join(", "); const authorNames = suggestedArticle.authorsMeta
return ( .map((author) => author.name)
<li .join(", ");
class={`${suggestedStyle.card} ${suggestedStyle.localCard}`} return (
> <li class={`${suggestedStyle.card} ${suggestedStyle.localCard}`}>
<a <a
href={`/${lang !== "en" ? `${lang}/` : ""}posts/${ href={`/${lang !== "en" ? `${lang}/` : ""}posts/${
suggestedArticle.slug suggestedArticle.slug
}`} }`}
data-type="Analytics link" data-type="Analytics link"
data-category="suggested_article" data-category="suggested_article"
class={suggestedStyle.aTag} class={suggestedStyle.aTag}
> >
<span class={suggestedStyle.titleTag}> <span class={suggestedStyle.titleTag}>
{suggestedArticle.title} {suggestedArticle.title}
</span> </span>
<br /> <br />
<span class={suggestedStyle.srOnly}>by</span> <span class={suggestedStyle.srOnly}>by</span>
<span class={suggestedStyle.author}> {authorNames}</span> <span class={suggestedStyle.author}> {authorNames}</span>
</a> </a>
</li> </li>
); );
})} })}
</ol> </ol>
</aside>} </aside>
)
}

View File

@@ -3,48 +3,48 @@
@import "../../../components/post-card/post-card.module"; @import "../../../components/post-card/post-card.module";
.list { .list {
list-style: none; list-style: none;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
.localCard { .localCard {
padding: 1rem !important; padding: 1rem !important;
margin-bottom: 1rem; margin-bottom: 1rem;
text-decoration: none; text-decoration: none;
} }
.localCard a { .localCard a {
text-decoration: none; text-decoration: none;
} }
.aTag { .aTag {
display: block; display: block;
} }
.localCard:hover .titleTag { .localCard:hover .titleTag {
text-decoration: underline; text-decoration: underline;
transition: text-decoration var(--animSpeed); transition: text-decoration var(--animSpeed);
} }
.header { .header {
@extend %headline-4; @extend %headline-4;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.author { .author {
color: var(--midImpactBlack) !important; color: var(--midImpactBlack) !important;
text-underline-style: none; text-underline-style: none;
} }
.srOnly { .srOnly {
position: absolute; position: absolute;
width: 1px; width: 1px;
height: 1px; height: 1px;
padding: 0; padding: 0;
margin: -1px; margin: -1px;
overflow: hidden; overflow: hidden;
clip: rect(0, 0, 0, 0); clip: rect(0, 0, 0, 0);
white-space: nowrap; white-space: nowrap;
border-width: 0; border-width: 0;
} }

View File

@@ -1,4 +1,4 @@
--- ---
--- ---
<script is:inline defer src="/scripts/tabs.min.js"/> <script is:inline defer src="/scripts/tabs.min.js"></script>

View File

@@ -1,9 +1,7 @@
--- ---
import * as Terser from "terser"; import * as Terser from "terser";
import { import { COLOR_MODE_STORAGE_KEY } from "../../constants";
COLOR_MODE_STORAGE_KEY,
} from "../../constants";
/** /**
* Much of this code deals with dark mode. It's ripped straight from: * Much of this code deals with dark mode. It's ripped straight from:
@@ -23,34 +21,36 @@ import {
* into that string. This way, we're able to avoid duplicating * into that string. This way, we're able to avoid duplicating
*/ */
function setColorsByTheme() { function setColorsByTheme() {
const STORAGE_KEY = "COLOR_MODE_STORAGE_KEY"; const STORAGE_KEY = "COLOR_MODE_STORAGE_KEY";
const mql = window.matchMedia("(prefers-color-scheme: dark)"); const mql = window.matchMedia("(prefers-color-scheme: dark)");
const prefersDarkFromMQ = mql.matches; const prefersDarkFromMQ = mql.matches;
const prefersDarkFromLocalStorage = localStorage.getItem(STORAGE_KEY); const prefersDarkFromLocalStorage = localStorage.getItem(STORAGE_KEY);
let colorMode = "light"; let colorMode = "light";
const hasUsedToggle = typeof prefersDarkFromLocalStorage === "string"; const hasUsedToggle = typeof prefersDarkFromLocalStorage === "string";
if (hasUsedToggle) { if (hasUsedToggle) {
colorMode = prefersDarkFromLocalStorage; colorMode = prefersDarkFromLocalStorage;
} else { } else {
colorMode = prefersDarkFromMQ ? "dark" : "light"; colorMode = prefersDarkFromMQ ? "dark" : "light";
} }
let root = document.documentElement; let root = document.documentElement;
// TODO: migrate to `classList` // TODO: migrate to `classList`
root.className = colorMode; root.className = colorMode;
} }
const boundFn = String(setColorsByTheme) const boundFn = String(setColorsByTheme).replace(
.replace("COLOR_MODE_STORAGE_KEY", COLOR_MODE_STORAGE_KEY); "COLOR_MODE_STORAGE_KEY",
COLOR_MODE_STORAGE_KEY
);
let calledFunction = `(${boundFn})()`; let calledFunction = `(${boundFn})()`;
calledFunction = (await Terser.minify(calledFunction)).code!; calledFunction = (await Terser.minify(calledFunction)).code!;
--- ---
<script set:html={calledFunction} /> <script set:html={calledFunction}></script>

View File

@@ -1,25 +1,25 @@
--- ---
import { import { COLORS } from "../../constants";
COLORS
} from "../../constants";
function getThemeStyling(theme: 'light' | 'dark') { function getThemeStyling(theme: "light" | "dark") {
const CSS_THEME = Object.entries(COLORS).reduce((prev, [key, val]) => { const CSS_THEME = Object.entries(COLORS).reduce((prev, [key, val]) => {
prev += `\n--${key}: ${val[theme]};`; prev += `\n--${key}: ${val[theme]};`;
return prev; return prev;
}, ""); }, "");
return CSS_THEME; return CSS_THEME;
} }
const rawStylesCSS = ` const rawStylesCSS = `
html.light, body.light { html.light, body.light {
${getThemeStyling('light')} ${getThemeStyling("light")}
} }
html.dark, body.dark { html.dark, body.dark {
${getThemeStyling('dark')} ${getThemeStyling("dark")}
} }
` `;
--- ---
<style set:html={rawStylesCSS} /> <style set:html={rawStylesCSS}>
</style>

View File

@@ -1,26 +1,38 @@
--- ---
import styles from "./post-list-header.module.scss"; import styles from "./post-list-header.module.scss";
import {Image} from '@astrojs/image/components'; import { Image } from "@astrojs/image/components";
import unicornLogo from "../../../assets/unicorn_utterances_logo_512.png"; import unicornLogo from "../../../assets/unicorn_utterances_logo_512.png";
interface PostListHeaderProps { interface PostListHeaderProps {
siteDescription: string; siteDescription: string;
} }
const { siteDescription } = Astro.props as PostListHeaderProps; const { siteDescription } = Astro.props as PostListHeaderProps;
--- ---
<div class={styles.container} role="banner" aria-label={`Banner for Unicorn Utterances`}> <div
<div class={styles.headerPic}> class={styles.container}
<Image format={'png'} loading={"eager"} sizes={"300px"} alt={`Smiling cartoon unicorn with a role="banner"
bowtie`} src={unicornLogo} height={300} width={300} /> aria-label={`Banner for Unicorn Utterances`}
</div> >
<div class={styles.noMgContainer}> <div class={styles.headerPic}>
<h1 class={styles.title}>Unicorn Utterances</h1> <Image
<div class={styles.subheader}> format={"png"}
{siteDescription} loading={"eager"}
<br /> sizes={"300px"}
<a href={"/about"}>About Us</a> alt={`Smiling cartoon unicorn with a
</div> bowtie`}
</div> src={unicornLogo}
height={300}
width={300}
/>
</div>
<div class={styles.noMgContainer}>
<h1 class={styles.title}>Unicorn Utterances</h1>
<div class={styles.subheader}>
{siteDescription}
<br />
<a href={"/about"}>About Us</a>
</div>
</div>
</div> </div>

View File

@@ -12,31 +12,30 @@ import { Page } from "astro";
import Pagination from "components/pagination/pagination.astro"; import Pagination from "components/pagination/pagination.astro";
export interface PostListTemplateProps { export interface PostListTemplateProps {
posts: PostInfo[]; posts: PostInfo[];
rootURL: string; rootURL: string;
page: Pick<Page<PostInfo>, 'total' | 'currentPage' | 'size' | 'lastPage' | 'url'> page: Pick<
Page<PostInfo>,
"total" | "currentPage" | "size" | "lastPage" | "url"
>;
} }
const { const { posts, page, rootURL } = Astro.props as PostListTemplateProps;
posts,
page,
rootURL
} = Astro.props as PostListTemplateProps;
--- ---
<div> <div>
<!-- <PostListProvider <!-- <PostListProvider
posts={posts} posts={posts}
numberOfPages={numberOfPages} numberOfPages={numberOfPages}
limitNumber={limitNumber} limitNumber={limitNumber}
pageIndex={pageIndex} pageIndex={pageIndex}
> --> > -->
<PostListHeader siteDescription={siteMetadata.description} /> <PostListHeader siteDescription={siteMetadata.description} />
<main> <main>
<!-- <FilterSearchBar /> --> <!-- <FilterSearchBar /> -->
<PostList listAriaLabel="List of posts" postsToDisplay={posts} /> <PostList listAriaLabel="List of posts" postsToDisplay={posts} />
</main> </main>
<Pagination page={page} rootURL={rootURL} /> <Pagination page={page} rootURL={rootURL} />
<!-- <Pagination absolutePath={router.basePath} /> --> <!-- <Pagination absolutePath={router.basePath} /> -->
<!-- </PostListProvider> --> <!-- </PostListProvider> -->
</div> </div>

View File

@@ -1,117 +1,115 @@
--- ---
import styles from "./profile-header.module.scss"; import styles from "./profile-header.module.scss";
import { UnicornInfo } from "uu-types"; import { UnicornInfo } from "uu-types";
import {Image} from '@astrojs/image/components'; import { Image } from "@astrojs/image/components";
import { Icon } from 'astro-icon'; import { Icon } from "astro-icon";
import SocialBtn from "./social-button.astro"; import SocialBtn from "./social-button.astro";
const getNamePossessive = (name: string) => { const getNamePossessive = (name: string) => {
if (name.endsWith("s")) return `${name}'`; if (name.endsWith("s")) return `${name}'`;
return `${name}'s`; return `${name}'s`;
}; };
interface PicTitleHeaderProps { interface PicTitleHeaderProps {
unicornData: UnicornInfo; unicornData: UnicornInfo;
} }
const { unicornData } = Astro.props as PicTitleHeaderProps; const { unicornData } = Astro.props as PicTitleHeaderProps;
const possessiveName = getNamePossessive(unicornData.name); const possessiveName = getNamePossessive(unicornData.name);
--- ---
<div <div
class={styles.container} class={styles.container}
role="banner" role="banner"
aria-label={`Banner for ${unicornData.name}`} aria-label={`Banner for ${unicornData.name}`}
> >
<div <div
class={styles.headerPic} class={styles.headerPic}
style={{ style={{
height: "300px", height: "300px",
width: "300px", width: "300px",
borderRadius: "50%", borderRadius: "50%",
overflow: "hidden", overflow: "hidden",
}} }}
> >
<Image <Image
src={unicornData.profileImgMeta.relativeServerPath} src={unicornData.profileImgMeta.relativeServerPath}
height={192} height={192}
width={192} width={192}
sizes={"192px"} sizes={"192px"}
format={'png'} format={"png"}
loading={"eager"} loading={"eager"}
alt={`${possessiveName} profile picture`} alt={`${possessiveName} profile picture`}
/> />
</div> </div>
<div class={styles.noMgContainer}> <div class={styles.noMgContainer}>
<h1 class={`${styles.title} ${styles.mobileTitle}`}> <h1 class={`${styles.title} ${styles.mobileTitle}`}>
{unicornData.name} {unicornData.name}
</h1> </h1>
<h1 class={`${styles.title} ${styles.desktopTitle}`}> <h1 class={`${styles.title} ${styles.desktopTitle}`}>
{unicornData.name} {unicornData.name}
</h1> </h1>
<div <div
class={styles.subheader} class={styles.subheader}
aria-label={`A description of ${unicornData.name}`} aria-label={`A description of ${unicornData.name}`}
> >
{unicornData.description} {unicornData.description}
</div> </div>
{unicornData.socials && ( {
<ul unicornData.socials && (
class={styles.socialsContainer} <ul
aria-label={`${possessiveName} social media links`} class={styles.socialsContainer}
role="list" aria-label={`${possessiveName} social media links`}
> role="list"
{unicornData.socials.twitter && ( >
<SocialBtn {unicornData.socials.twitter && (
text={"Twitter"} <SocialBtn
url={`https://twitter.com/${unicornData.socials.twitter}`} text={"Twitter"}
> url={`https://twitter.com/${unicornData.socials.twitter}`}
<Icon name="twitter" height="36" width="36" slot="icon"/> >
</SocialBtn> <Icon name="twitter" height="36" width="36" slot="icon" />
)} </SocialBtn>
{unicornData.socials.github && ( )}
<SocialBtn {unicornData.socials.github && (
text={"GitHub"} <SocialBtn
url={`https://github.com/${unicornData.socials.github}`} text={"GitHub"}
> url={`https://github.com/${unicornData.socials.github}`}
<Icon name="github" height="36" width="36" slot="icon"/> >
</SocialBtn> <Icon name="github" height="36" width="36" slot="icon" />
)} </SocialBtn>
{unicornData.socials.linkedIn && ( )}
<SocialBtn {unicornData.socials.linkedIn && (
text={"LinkedIn"} <SocialBtn
url={`https://www.linkedin.com/in/${unicornData.socials.linkedIn}`} text={"LinkedIn"}
> url={`https://www.linkedin.com/in/${unicornData.socials.linkedIn}`}
<Icon name="linkedin" height="36" width="36" slot="icon"/> >
</SocialBtn> <Icon name="linkedin" height="36" width="36" slot="icon" />
)} </SocialBtn>
{unicornData.socials.twitch && ( )}
<SocialBtn {unicornData.socials.twitch && (
text={"Twitch"} <SocialBtn
url={`https://twitch.tv/${unicornData.socials.twitch}`} text={"Twitch"}
> url={`https://twitch.tv/${unicornData.socials.twitch}`}
<Icon name="twitch" height="36" width="36" slot="icon"/> >
</SocialBtn> <Icon name="twitch" height="36" width="36" slot="icon" />
)} </SocialBtn>
{unicornData.socials.dribbble && ( )}
<SocialBtn {unicornData.socials.dribbble && (
text={"Dribbble"} <SocialBtn
url={`https://dribbble.com/${unicornData.socials.dribbble}`} text={"Dribbble"}
> url={`https://dribbble.com/${unicornData.socials.dribbble}`}
<Icon name="dribbble" height="36" width="36" slot="icon"/> >
</SocialBtn> <Icon name="dribbble" height="36" width="36" slot="icon" />
)} </SocialBtn>
{unicornData.socials.website && ( )}
<SocialBtn {unicornData.socials.website && (
text={"Website"} <SocialBtn text={"Website"} url={unicornData.socials.website}>
url={unicornData.socials.website} <Icon name="site" height="36" width="36" slot="icon" />
> </SocialBtn>
<Icon name="site" height="36" width="36" slot="icon"/> )}
</SocialBtn> </ul>
)} )
</ul> }
)} </div>
</div> </div>
</div>

View File

@@ -1,29 +1,24 @@
--- ---
interface SocialBtnProps { interface SocialBtnProps {
text: string; text: string;
url: string; url: string;
} }
const { text, url } = Astro.props as SocialBtnProps; const { text, url } = Astro.props as SocialBtnProps;
import styles from './social-button.module.scss'; import styles from "./social-button.module.scss";
--- ---
<li class={`baseBtn ${styles.socialBtnLink}`} role="listitem"> <li class={`baseBtn ${styles.socialBtnLink}`} role="listitem">
<!-- <AnalyticsLink <!-- <AnalyticsLink
category="outbound" category="outbound"
class="unlink" class="unlink"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
href={url} href={url}
> --> > -->
<a <a class="unlink" target="_blank" rel="noreferrer" href={url}>
class="unlink" <span class={styles.svgContainer} aria-hidden={true}>
target="_blank" <slot name="icon" />
rel="noreferrer" </span>
href={url} <span class={styles.socialText}>{text}</span>
> </a>
<span class={styles.svgContainer} aria-hidden={true}>
<slot name="icon"/>
</span>
<span class={styles.socialText}>{text}</span>
</a>
</li> </li>

View File

@@ -8,7 +8,6 @@
align-items: center; align-items: center;
} }
.socialBtnLink { .socialBtnLink {
&:not(:last-of-type) { &:not(:last-of-type) {
margin-right: 32px; margin-right: 32px;

View File

@@ -3,24 +3,21 @@ import PostList from "components/post-card-list/post-card-list.astro";
import Pagination from "components/pagination/pagination.astro"; import Pagination from "components/pagination/pagination.astro";
import { Page } from "astro"; import { Page } from "astro";
import { PostInfo } from "types/PostInfo"; import { PostInfo } from "types/PostInfo";
import ProfileHeader from './profile-header/profile-header.astro'; import ProfileHeader from "./profile-header/profile-header.astro";
export interface UnicornTemplateProps { export interface UnicornTemplateProps {
unicorn: any; unicorn: any;
posts: PostInfo[]; posts: PostInfo[];
rootURL: string; rootURL: string;
page: Pick<Page<PostInfo>, 'total' | 'currentPage' | 'size' | 'lastPage' | 'url'> page: Pick<
Page<PostInfo>,
"total" | "currentPage" | "size" | "lastPage" | "url"
>;
} }
const { const { unicorn, page, rootURL, posts } = Astro.props as UnicornTemplateProps;
unicorn,
page,
rootURL,
posts
} = Astro.props as UnicornTemplateProps;
--- ---
<!-- <PostListProvider <!-- <PostListProvider
posts={authoredPosts} posts={authoredPosts}
numberOfPages={numberOfPages} numberOfPages={numberOfPages}
@@ -29,16 +26,16 @@ pageIndex={pageNum}
> --> > -->
<ProfileHeader unicornData={unicorn} /> <ProfileHeader unicornData={unicorn} />
<main> <main>
<!-- <FilterSearchBar> <!-- <FilterSearchBar>
<WordCount <WordCount
wordCount={wordCount} wordCount={wordCount}
numberOfArticles={authoredPosts.length} numberOfArticles={authoredPosts.length}
/> />
</FilterSearchBar> --> </FilterSearchBar> -->
<PostList <PostList
listAriaLabel={`List of posts written by ${unicorn.name}`} listAriaLabel={`List of posts written by ${unicorn.name}`}
postsToDisplay={posts} postsToDisplay={posts}
/> />
</main> </main>
<Pagination page={page} rootURL={rootURL} /> <Pagination page={page} rootURL={rootURL} />
<!-- <Pagination absolutePath={basePath} /> <!-- <Pagination absolutePath={basePath} />

View File

@@ -1,13 +1,15 @@
--- ---
import Document from '../layouts/document.astro'; import Document from "../layouts/document.astro";
import PostListTemplate, { PostListTemplateProps } from '../page-components/post-list/post-list.astro'; import PostListTemplate, {
import { getAllPostsForListView } from 'utils/api'; PostListTemplateProps,
import {PostInfo} from 'types/PostInfo'; } from "../page-components/post-list/post-list.astro";
import { getAllPostsForListView } from "utils/api";
import { PostInfo } from "types/PostInfo";
import SEO from "components/seo/seo.astro"; import SEO from "components/seo/seo.astro";
const posts = await Astro.glob<PostInfo>('../../content/blog/**/*.md') const posts = await Astro.glob<PostInfo>("../../content/blog/**/*.md");
const enPosts = getAllPostsForListView(posts, 'en'); const enPosts = getAllPostsForListView(posts, "en");
const postsToDisplay = enPosts.slice(0, 8); const postsToDisplay = enPosts.slice(0, 8);
const page = { const page = {
@@ -17,12 +19,12 @@ const page = {
lastPage: Math.floor(enPosts.length / postsToDisplay.length), lastPage: Math.floor(enPosts.length / postsToDisplay.length),
url: { url: {
current: Astro.url.href, current: Astro.url.href,
next: '/page/2' next: "/page/2",
} },
} as PostListTemplateProps['page']; } as PostListTemplateProps["page"];
--- ---
<Document> <Document>
<SEO slot="head" title="Homepage" /> <SEO slot="head" title="Homepage" />
<PostListTemplate posts={postsToDisplay} page={page} rootURL={'/'}/> <PostListTemplate posts={postsToDisplay} page={page} rootURL={"/"} />
</Document> </Document>

View File

@@ -1,20 +1,20 @@
--- ---
import Document from '../../layouts/document.astro'; import Document from "../../layouts/document.astro";
import PostListTemplate from '../../page-components/post-list/post-list.astro'; import PostListTemplate from "../../page-components/post-list/post-list.astro";
import { getAllPostsForListView } from 'utils/api'; import { getAllPostsForListView } from "utils/api";
import {PostInfo} from 'types/PostInfo'; import { PostInfo } from "types/PostInfo";
import SEO from "components/seo/seo.astro"; import SEO from "components/seo/seo.astro";
import { Page } from 'astro'; import { Page } from "astro";
export async function getStaticPaths({ paginate }) { export async function getStaticPaths({ paginate }) {
const posts = await Astro.glob<PostInfo>('../../../content/blog/**/*.md') const posts = await Astro.glob<PostInfo>("../../../content/blog/**/*.md");
const postsToDisplay = getAllPostsForListView(posts, 'en'); const postsToDisplay = getAllPostsForListView(posts, "en");
return paginate(postsToDisplay, { pageSize: 8 }); return paginate(postsToDisplay, { pageSize: 8 });
} }
const { page } = Astro.props as {page: Page<PostInfo>}; const { page } = Astro.props as { page: Page<PostInfo> };
const pageIndex = page.currentPage; const pageIndex = page.currentPage;
@@ -23,5 +23,5 @@ const SEOTitle = `Post page ${pageIndex}`;
<Document> <Document>
<SEO slot="head" title={SEOTitle} /> <SEO slot="head" title={SEOTitle} />
<PostListTemplate posts={page.data} page={page} rootURL={'/'}/> <PostListTemplate posts={page.data} page={page} rootURL={"/"} />
</Document> </Document>

View File

@@ -1,83 +1,84 @@
--- ---
import Document from '../../layouts/document.astro'; import Document from "../../layouts/document.astro";
import SEO from "components/seo/seo.astro"; import SEO from "components/seo/seo.astro";
import BlogPostLayout from 'components/blog-post-layout/blog-post-layout.astro'; import BlogPostLayout from "components/blog-post-layout/blog-post-layout.astro";
import PostTitleHeader from 'src/page-components/blog-post/post-title-header/post-title-header.astro'; import PostTitleHeader from "src/page-components/blog-post/post-title-header/post-title-header.astro";
import PostMetadata from 'src/page-components/blog-post/post-metadata/post-metadata.astro'; import PostMetadata from "src/page-components/blog-post/post-metadata/post-metadata.astro";
import TabsScript from 'src/page-components/blog-post/tabs-script/tabs-script.astro'; import TabsScript from "src/page-components/blog-post/tabs-script/tabs-script.astro";
import SuggestedArticles from 'src/page-components/blog-post/suggested-articles/suggested-articles.astro'; import SuggestedArticles from "src/page-components/blog-post/suggested-articles/suggested-articles.astro";
import TableOfContents from 'components/table-of-contents/table-of-contents.astro'; import TableOfContents from "components/table-of-contents/table-of-contents.astro";
import type { MarkdownInstance } from 'astro'; import type { MarkdownInstance } from "astro";
import {PostInfo} from 'types/PostInfo'; import { PostInfo } from "types/PostInfo";
import { Languages } from 'types/index'; import { Languages } from "types/index";
export async function getStaticPaths() { export async function getStaticPaths() {
const posts = await Astro.glob<PostInfo>('../../../content/blog/**/*.md') const posts = await Astro.glob<PostInfo>("../../../content/blog/**/*.md");
return posts.map(post => { return posts.map((post) => {
return { return {
params: { params: {
// TODO: Pass locale // TODO: Pass locale
postid: post.frontmatter.slug postid: post.frontmatter.slug,
}, },
props: { props: {
Content: post.Content, Content: post.Content,
post: post.frontmatter post: post.frontmatter,
} },
} };
}) });
} }
const { Content, post } = Astro.props as { const { Content, post } = Astro.props as {
post: PostInfo, post: PostInfo;
Content: MarkdownInstance<any>['Content'] Content: MarkdownInstance<any>["Content"];
} };
const translations = post?.translations || []; const translations = post?.translations || [];
const otherLangs = translations const otherLangs = translations
? (Object.keys(translations).filter( ? (Object.keys(translations).filter((t) => t !== post.locale) as Languages[])
(t) => t !== post.locale : [];
) as Languages[])
: [];
--- ---
<Document> <Document>
<TabsScript slot="head"/> <TabsScript slot="head" />
<!-- TODO: Add this --> <!-- TODO: Add this -->
<!-- pathName={router.asPath} --> <!-- pathName={router.asPath} -->
<SEO <SEO
slot="head" slot="head"
title={post.title} title={post.title}
description={post.description || post.excerpt} description={post.description || post.excerpt}
unicornsData={post.authors} unicornsData={post.authors}
publishedTime={post.published} publishedTime={post.published}
editedTime={post.edited} editedTime={post.edited}
keywords={post.tags} keywords={post.tags}
type="article" type="article"
canonical={post.originalLink} canonical={post.originalLink}
langData={{ langData={{
otherLangs, otherLangs,
currentLang: post.locale, currentLang: post.locale,
}} }}
shareImage={`/${post.slug}.twitter-preview.png`} shareImage={`/${post.slug}.twitter-preview.png`}
/> />
<article> <article>
<BlogPostLayout> <BlogPostLayout>
<div slot="left"> <div slot="left">
<TableOfContents headingsWithId={post.headingsWithId} /> <TableOfContents headingsWithId={post.headingsWithId} />
</div> </div>
<div slot="right"> <div slot="right">
<SuggestedArticles suggestedArticles={post.suggestedArticles} lang={"en"} /> <SuggestedArticles
</div> suggestedArticles={post.suggestedArticles}
<header role="banner" class="marginZeroAutoChild"> lang={"en"}
<PostTitleHeader post={post} /> />
<PostMetadata post={post} /> </div>
</header> <header role="banner" class="marginZeroAutoChild">
<main class="post-body" data-testid={"post-body-div"}> <PostTitleHeader post={post} />
<!-- {post.series ? ( <PostMetadata post={post} />
</header>
<main class="post-body" data-testid={"post-body-div"}>
<!-- {post.series ? (
<SeriesToC <SeriesToC
post={post} post={post}
postSeries={seriesPosts} postSeries={seriesPosts}
@@ -87,11 +88,11 @@ const otherLangs = translations
{post.translations && Object.keys(post.translations).length ? ( {post.translations && Object.keys(post.translations).length ? (
<TranslationsHeader post={post} /> <TranslationsHeader post={post} />
) : null} --> ) : null} -->
<Content/> <Content />
<!-- {post.series ? ( <!-- {post.series ? (
<SeriesNav post={post} postSeries={seriesPosts} /> <SeriesNav post={post} postSeries={seriesPosts} />
) : null} --> ) : null} -->
</main> </main>
</BlogPostLayout> </BlogPostLayout>
</article> </article>
</Document> </Document>

View File

@@ -1,22 +1,22 @@
--- ---
import Document from '../../../layouts/document.astro'; import Document from "../../../layouts/document.astro";
import SEO from "components/seo/seo.astro"; import SEO from "components/seo/seo.astro";
import UnicornsPage from '../../../page-components/unicorns/unicorn-page.astro'; import UnicornsPage from "../../../page-components/unicorns/unicorn-page.astro";
import { getAllPostsForUnicornListView } from 'utils/api'; import { getAllPostsForUnicornListView } from "utils/api";
import {PostInfo} from 'types/PostInfo'; import { PostInfo } from "types/PostInfo";
import { unicorns } from "utils/data"; import { unicorns } from "utils/data";
import { Page } from 'astro'; import { Page } from "astro";
export async function getStaticPaths() { export async function getStaticPaths() {
return unicorns.map(unicorn => ({ params: {unicornid: unicorn.id}})) return unicorns.map((unicorn) => ({ params: { unicornid: unicorn.id } }));
} }
const params = Astro.params as {unicornid: string}; const params = Astro.params as { unicornid: string };
const unicorn = unicorns.find(unicorn => unicorn.id === params.unicornid); const unicorn = unicorns.find((unicorn) => unicorn.id === params.unicornid);
const posts = await Astro.glob<PostInfo>('../../../../content/blog/**/*.md') const posts = await Astro.glob<PostInfo>("../../../../content/blog/**/*.md");
const enPosts = getAllPostsForUnicornListView(unicorn.id, posts, 'en'); const enPosts = getAllPostsForUnicornListView(unicorn.id, posts, "en");
const postsToDisplay = enPosts.slice(0, 8); const postsToDisplay = enPosts.slice(0, 8);
const page = { const page = {
@@ -26,20 +26,25 @@ const page = {
lastPage: Math.floor(enPosts.length / postsToDisplay.length), lastPage: Math.floor(enPosts.length / postsToDisplay.length),
url: { url: {
current: `/unicorns/${unicorn.id}`, current: `/unicorns/${unicorn.id}`,
next: `/unicorns/${unicorn.id}/page/2` next: `/unicorns/${unicorn.id}/page/2`,
} },
} as Page<PostInfo>; } as Page<PostInfo>;
const rootURL = `/unicorns/${unicorn.id}/`; const rootURL = `/unicorns/${unicorn.id}/`;
--- ---
<Document> <Document>
<SEO <SEO
slot="head" slot="head"
title={unicorn.name} title={unicorn.name}
description={unicorn.description} description={unicorn.description}
unicornsData={[unicorn]} unicornsData={[unicorn]}
type="profile" type="profile"
/> />
<UnicornsPage unicorn={unicorn} page={page} posts={postsToDisplay} rootURL={rootURL}/> <UnicornsPage
unicorn={unicorn}
page={page}
posts={postsToDisplay}
rootURL={rootURL}
/>
</Document> </Document>

View File

@@ -1,34 +1,48 @@
--- ---
import Document from '../../../../layouts/document.astro'; import Document from "../../../../layouts/document.astro";
import SEO from "components/seo/seo.astro"; import SEO from "components/seo/seo.astro";
import UnicornsPage from '../../../../page-components/unicorns/unicorn-page.astro'; import UnicornsPage from "../../../../page-components/unicorns/unicorn-page.astro";
import { getAllPostsForUnicornListView } from 'utils/api'; import { getAllPostsForUnicornListView } from "utils/api";
import {PostInfo} from 'types/PostInfo'; import { PostInfo } from "types/PostInfo";
import { unicorns } from "utils/data"; import { unicorns } from "utils/data";
import { Page } from 'astro'; import { Page } from "astro";
export async function getStaticPaths({ paginate }) { export async function getStaticPaths({ paginate }) {
const posts = await Astro.glob<PostInfo>('../../../../../content/blog/**/*.md') const posts = await Astro.glob<PostInfo>(
return unicorns.map(unicorn => { "../../../../../content/blog/**/*.md"
const postsToDisplay = getAllPostsForUnicornListView(unicorn.id, posts, 'en'); );
return paginate(postsToDisplay, { params: {unicornid: unicorn.id}, pageSize: 8 }); return unicorns.map((unicorn) => {
}) const postsToDisplay = getAllPostsForUnicornListView(
unicorn.id,
posts,
"en"
);
return paginate(postsToDisplay, {
params: { unicornid: unicorn.id },
pageSize: 8,
});
});
} }
const { page } = Astro.props as {page: Page<PostInfo>}; const { page } = Astro.props as { page: Page<PostInfo> };
const params = Astro.params as {unicornid: string}; const params = Astro.params as { unicornid: string };
const unicorn = unicorns.find(unicorn => unicorn.id === params.unicornid); const unicorn = unicorns.find((unicorn) => unicorn.id === params.unicornid);
const rootURL = `/unicorns/${unicorn.id}/`; const rootURL = `/unicorns/${unicorn.id}/`;
--- ---
<Document> <Document>
<SEO <SEO
slot="head" slot="head"
title={unicorn.name} title={unicorn.name}
description={unicorn.description} description={unicorn.description}
unicornsData={[unicorn]} unicornsData={[unicorn]}
type="profile" type="profile"
/> />
<UnicornsPage unicorn={unicorn} page={page} posts={page.data} rootURL={rootURL}/> <UnicornsPage
unicorn={unicorn}
page={page}
posts={page.data}
rootURL={rootURL}
/>
</Document> </Document>

View File

@@ -15,7 +15,7 @@ code .line::before {
margin-right: 1.5rem; margin-right: 1.5rem;
display: inline-block; display: inline-block;
text-align: right; text-align: right;
color: rgba(115,138,148,.4) color: rgba(115, 138, 148, 0.4);
} }
/* Start of Shiki Twoslash CSS: /* Start of Shiki Twoslash CSS:
@@ -77,7 +77,8 @@ pre.shiki:hover .dim {
pre.shiki div.dim { pre.shiki div.dim {
opacity: 0.5; opacity: 0.5;
} }
pre.shiki div.dim, pre.shiki div.highlight { pre.shiki div.dim,
pre.shiki div.highlight {
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
@@ -90,7 +91,7 @@ pre.shiki div.line {
} }
/** Don't show the language identifiers */ /** Don't show the language identifiers */
pre.shiki .language-id{ pre.shiki .language-id {
display: none; display: none;
} }
@@ -163,7 +164,8 @@ pre code a {
} }
pre data-err { pre data-err {
/* Extracted from VS Code */ /* Extracted from VS Code */
background: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%206%203'%20enable-background%3D'new%200%200%206%203'%20height%3D'3'%20width%3D'6'%3E%3Cg%20fill%3D'%23c94824'%3E%3Cpolygon%20points%3D'5.5%2C0%202.5%2C3%201.1%2C3%204.1%2C0'%2F%3E%3Cpolygon%20points%3D'4%2C0%206%2C2%206%2C0.6%205.4%2C0'%2F%3E%3Cpolygon%20points%3D'0%2C2%201%2C3%202.4%2C3%200%2C0.6'%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E") repeat-x bottom left; background: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%206%203'%20enable-background%3D'new%200%200%206%203'%20height%3D'3'%20width%3D'6'%3E%3Cg%20fill%3D'%23c94824'%3E%3Cpolygon%20points%3D'5.5%2C0%202.5%2C3%201.1%2C3%204.1%2C0'%2F%3E%3Cpolygon%20points%3D'4%2C0%206%2C2%206%2C0.6%205.4%2C0'%2F%3E%3Cpolygon%20points%3D'0%2C2%201%2C3%202.4%2C3%200%2C0.6'%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E")
repeat-x bottom left;
padding-bottom: 3px; padding-bottom: 3px;
} }
pre .query { pre .query {
@@ -178,7 +180,8 @@ pre .query {
/* This sections keeps both of those two in in sync */ /* This sections keeps both of those two in in sync */
pre .error, pre .error-behind { pre .error,
pre .error-behind {
margin-left: -14px; margin-left: -14px;
margin-top: 8px; margin-top: 8px;
margin-bottom: 4px; margin-bottom: 4px;
@@ -346,7 +349,6 @@ pre .logger.log-log svg {
margin-right: 9px; margin-right: 9px;
} }
blockquote { blockquote {
position: relative; position: relative;
margin-left: 1em; margin-left: 1em;

View File

@@ -132,7 +132,6 @@ $robotoMono: "Roboto Mono", monospace;
} }
} }
%headline-uniwidth-2 { %headline-uniwidth-2 {
font-family: $asap; font-family: $asap;
font-weight: 400; font-weight: 400;

View File

@@ -1,59 +1,59 @@
.tabs { .tabs {
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
&__tab-list { &__tab-list {
padding: 0; padding: 0;
margin: 0 1rem -2px; margin: 0 1rem -2px;
} }
&__tab { &__tab {
display: inline-block; display: inline-block;
border: 1px solid transparent; border: 1px solid transparent;
border-bottom: none; border-bottom: none;
bottom: -2px; bottom: -2px;
position: relative; position: relative;
list-style: none; list-style: none;
padding: 6px 12px; padding: 6px 12px;
cursor: pointer; cursor: pointer;
background: var(--darkPrimary); background: var(--darkPrimary);
color: var(--backgroundColor); color: var(--backgroundColor);
border-radius: 0.5rem 0.5rem 0 0; border-radius: 0.5rem 0.5rem 0 0;
margin-right: 4px; margin-right: 4px;
&[aria-selected="true"] { &[aria-selected="true"] {
background: var(--backgroundColor); background: var(--backgroundColor);
border: 2px solid var(--darkPrimary); border: 2px solid var(--darkPrimary);
border-bottom: 0px; border-bottom: 0px;
margin-bottom: 2px; margin-bottom: 2px;
color: var(--darkPrimary); color: var(--darkPrimary);
} }
&[aria-disabled="true"] { &[aria-disabled="true"] {
color: GrayText; color: GrayText;
cursor: default; cursor: default;
} }
&:focus { &:focus {
box-shadow: 0 0 5px hsl(208, 99%, 50%); box-shadow: 0 0 5px hsl(208, 99%, 50%);
border-color: hsl(208, 99%, 50%); border-color: hsl(208, 99%, 50%);
outline: none; outline: none;
&:after { &:after {
content: ""; content: "";
position: absolute; position: absolute;
height: 5px; height: 5px;
left: -4px; left: -4px;
right: -4px; right: -4px;
bottom: -5px; bottom: -5px;
background: var(--backgroundColor); background: var(--backgroundColor);
} }
} }
} }
&__tab-panel { &__tab-panel {
border: 2px solid var(--darkPrimary); border: 2px solid var(--darkPrimary);
border-radius: 0.5rem; border-radius: 0.5rem;
padding: 1.5rem; padding: 1.5rem;
} }
} }

View File

@@ -2,37 +2,37 @@ import { UnicornInfo } from "./UnicornInfo";
import { PostInfo } from "types/PostInfo"; import { PostInfo } from "types/PostInfo";
export interface CollectionInfo { export interface CollectionInfo {
slug: string; slug: string;
title: string; title: string;
authors: UnicornInfo[]; authors: UnicornInfo[];
description: string; description: string;
associatedSeries: string; associatedSeries: string;
published: string; published: string;
isbn?: string; isbn?: string;
type?: "book"; type?: "book";
coverImg: { coverImg: {
height: number; height: number;
width: number; width: number;
relativePath: string; relativePath: string;
}; };
socialImg?: string; socialImg?: string;
posts: Pick< posts: Pick<
PostInfo, PostInfo,
| "description" | "description"
| "excerpt" | "excerpt"
| "title" | "title"
| "order" | "order"
| "series" | "series"
| "slug" | "slug"
| "authors" | "authors"
| "content" | "content"
>[]; >[];
content: string; content: string;
buttons: Array<{ text: string; url: string }>; buttons: Array<{ text: string; url: string }>;
chapterList?: Array<{ chapterList?: Array<{
title: string; title: string;
description: string; description: string;
order: string; order: string;
}>; }>;
aboveFoldMarkdown?: string; aboveFoldMarkdown?: string;
} }

View File

@@ -1,8 +1,8 @@
export interface LicenseInfo { export interface LicenseInfo {
id: string; id: string;
licenceType: string; licenceType: string;
footerImg: string; footerImg: string;
explainLink: string; explainLink: string;
name: string; name: string;
displayName: string; displayName: string;
} }

View File

@@ -4,35 +4,35 @@ import { Languages } from "types/index";
import { MarkdownInstance } from "astro"; import { MarkdownInstance } from "astro";
export interface RawPostInfo { export interface RawPostInfo {
title: string; title: string;
published: string; published: string;
authors: string[]; authors: string[];
tags: string[]; tags: string[];
attached: string[]; attached: string[];
license: string; license: string;
description?: string; description?: string;
edited?: string; edited?: string;
series?: string; series?: string;
order?: number; order?: number;
originalLink?: string; originalLink?: string;
} }
export interface PostInfo extends RawPostInfo { export interface PostInfo extends RawPostInfo {
slug: string; slug: string;
locale: Languages; locale: Languages;
Content: MarkdownInstance<never>['Content']; Content: MarkdownInstance<never>["Content"];
authorsMeta: UnicornInfo[]; authorsMeta: UnicornInfo[];
licenseMeta: LicenseInfo; licenseMeta: LicenseInfo;
excerpt: string; excerpt: string;
wordCount: number; wordCount: number;
collectionSlug?: string | null; collectionSlug?: string | null;
translations: Partial<Record<Languages, string>>; translations: Partial<Record<Languages, string>>;
suggestedArticles: [PostInfo, PostInfo, PostInfo]; suggestedArticles: [PostInfo, PostInfo, PostInfo];
headingsWithId?: Array<{ headingsWithId?: Array<{
// Title value // Title value
value: string; value: string;
// ID // ID
slug: string; slug: string;
depth: number; depth: number;
}>; }>;
} }

View File

@@ -1,8 +1,8 @@
export interface PronounInfo { export interface PronounInfo {
id: string; id: string;
they: string; they: string;
them: string; them: string;
their: string; their: string;
theirs: string; theirs: string;
themselves: string; themselves: string;
} }

View File

@@ -1,25 +1,25 @@
export type RolesEnum = export type RolesEnum =
| { | {
id: "developer"; id: "developer";
prettyname: "Developer"; prettyname: "Developer";
} }
| { | {
id: "designer"; id: "designer";
prettyname: "Designer"; prettyname: "Designer";
} }
| { | {
id: "devops"; id: "devops";
prettyname: "Dev-ops"; prettyname: "Dev-ops";
} }
| { | {
id: "author"; id: "author";
prettyname: "Author"; prettyname: "Author";
} }
| { | {
id: "translator"; id: "translator";
prettyname: "Translator"; prettyname: "Translator";
} }
| { | {
id: "community"; id: "community";
prettyname: "Community Leader"; prettyname: "Community Leader";
}; };

View File

@@ -2,36 +2,36 @@ import { PronounInfo } from "./PronounInfo";
import { RolesEnum } from "./RolesInfo"; import { RolesEnum } from "./RolesInfo";
export interface RawUnicornInfo { export interface RawUnicornInfo {
id: string; id: string;
name: string; name: string;
firstName: string; firstName: string;
lastName: string; lastName: string;
description: string; description: string;
socials: { socials: {
twitter?: string; twitter?: string;
github?: string; github?: string;
website?: string; website?: string;
linkedIn?: string; linkedIn?: string;
twitch?: string; twitch?: string;
dribbble?: string; dribbble?: string;
}; };
pronouns: string; pronouns: string;
profileImg: string; profileImg: string;
color: string; color: string;
roles: Array<RolesEnum['id']>; roles: Array<RolesEnum["id"]>;
} }
export interface UnicornInfo extends RawUnicornInfo { export interface UnicornInfo extends RawUnicornInfo {
rolesMeta: RolesEnum[]; rolesMeta: RolesEnum[];
pronounsMeta: PronounInfo; pronounsMeta: PronounInfo;
profileImgMeta: { profileImgMeta: {
// Relative to "public/unicorns" // Relative to "public/unicorns"
relativePath: string; relativePath: string;
// Relative to site root // Relative to site root
relativeServerPath: string; relativeServerPath: string;
// This is not stored, it's generated at build time // This is not stored, it's generated at build time
absoluteFSPath: string; absoluteFSPath: string;
height: number; height: number;
width: number; width: number;
}; };
} }

View File

@@ -1,6 +1,6 @@
declare module "gatsby-remark-embedder/dist/transformers/Twitch.js" { declare module "gatsby-remark-embedder/dist/transformers/Twitch.js" {
import { Transformer } from "@remark-embedder/core"; import { Transformer } from "@remark-embedder/core";
const transformer: Transformer<any>; const transformer: Transformer<any>;
export = transformer; export = transformer;
} }

View File

@@ -1,14 +1,14 @@
declare module "remark-behead" { declare module "remark-behead" {
import type { Node } from "unist"; import type { Node } from "unist";
import type { Root } from "mdast"; import type { Root } from "mdast";
import type { Plugin } from "unified"; import type { Plugin } from "unified";
export interface BeheadOptions { export interface BeheadOptions {
depth: number; depth: number;
after: number | string | Node; after: number | string | Node;
before: number | string | Node; before: number | string | Node;
between: [number | string | Node, number | string | Node]; between: [number | string | Node, number | string | Node];
} }
declare const plugin: Plugin<[BeheadOptions?] | void[], Root, string>; declare const plugin: Plugin<[BeheadOptions?] | void[], Root, string>;
export default plugin; export default plugin;
} }

View File

@@ -1,11 +1,11 @@
import lunr from "lunr"; import lunr from "lunr";
declare global { declare global {
interface Window { interface Window {
__LUNR__: { __LUNR__: {
index: lunr.Index; index: lunr.Index;
store: Record<string, any>; store: Record<string, any>;
__loaded: boolean | Promise<void>; __loaded: boolean | Promise<void>;
}; };
} }
} }

View File

@@ -2,49 +2,49 @@ import { PostInfo } from "types/PostInfo";
import { Languages } from "types/index"; import { Languages } from "types/index";
import { MarkdownInstance } from "astro"; import { MarkdownInstance } from "astro";
let allPostsCache = new WeakMap<object, MarkdownInstance<PostInfo>[]>(); const allPostsCache = new WeakMap<object, MarkdownInstance<PostInfo>[]>();
export function getAllPosts( export function getAllPosts(
posts: MarkdownInstance<PostInfo>[], posts: MarkdownInstance<PostInfo>[],
language: Languages, language: Languages,
cacheString: null | object = null cacheString: null | object = null
): MarkdownInstance<PostInfo>[] { ): MarkdownInstance<PostInfo>[] {
if (cacheString) { if (cacheString) {
const cacheData = allPostsCache.get(cacheString); const cacheData = allPostsCache.get(cacheString);
if (cacheData) return cacheData as any; if (cacheData) return cacheData as any;
} }
if (cacheString) allPostsCache.set(cacheString, posts); if (cacheString) allPostsCache.set(cacheString, posts);
return posts return posts.filter((post) => post.frontmatter.locale === language);
.filter(post => post.frontmatter.locale === language);
} }
const listViewCache = {}; const listViewCache = {};
export const getAllPostsForListView = ( export const getAllPostsForListView = (
posts: MarkdownInstance<PostInfo>[], posts: MarkdownInstance<PostInfo>[],
language: Languages, language: Languages
): PostInfo[] => { ): PostInfo[] => {
let allPosts = getAllPosts(posts, language, listViewCache); let allPosts = getAllPosts(posts, language, listViewCache);
// sort posts by date in descending order // sort posts by date in descending order
allPosts = allPosts.sort((post1, post2) => { allPosts = allPosts.sort((post1, post2) => {
const date1 = new Date(post1.frontmatter.published); const date1 = new Date(post1.frontmatter.published);
const date2 = new Date(post2.frontmatter.published); const date2 = new Date(post2.frontmatter.published);
return date1 > date2 ? -1 : 1; return date1 > date2 ? -1 : 1;
}); });
return allPosts.map(post => post.frontmatter).filter(post => post.locale === language); return allPosts
.map((post) => post.frontmatter)
.filter((post) => post.locale === language);
}; };
export const getAllPostsForUnicornListView = ( export const getAllPostsForUnicornListView = (
authorId: string, authorId: string,
posts: MarkdownInstance<PostInfo>[], posts: MarkdownInstance<PostInfo>[],
language: Languages, language: Languages
): PostInfo[] => { ): PostInfo[] => {
return getAllPostsForListView(posts, language) return getAllPostsForListView(posts, language).filter((post) =>
.filter(post => post.authorsMeta.find((postAuthor) => postAuthor.id === authorId)
post.authorsMeta.find(postAuthor => postAuthor.id === authorId) );
);
}; };

View File

@@ -11,63 +11,61 @@ export const siteDirectory = join(process.cwd(), "content/site");
export const sponsorsDirectory = join(process.cwd(), "public/sponsors"); export const sponsorsDirectory = join(process.cwd(), "public/sponsors");
const unicornsRaw: Array< const unicornsRaw: Array<
Omit<UnicornInfo, "roles" | "pronouns" | "profileImg"> & { Omit<UnicornInfo, "roles" | "pronouns" | "profileImg"> & {
roles: string[]; roles: string[];
pronouns: string; pronouns: string;
profileImg: string; profileImg: string;
} }
> = JSON.parse( > = JSON.parse(
fs.readFileSync(join(dataDirectory, "unicorns.json")).toString() fs.readFileSync(join(dataDirectory, "unicorns.json")).toString()
); );
const rolesRaw: RolesEnum[] = JSON.parse( const rolesRaw: RolesEnum[] = JSON.parse(
fs.readFileSync(join(dataDirectory, "roles.json")).toString() fs.readFileSync(join(dataDirectory, "roles.json")).toString()
); );
const pronounsRaw: PronounInfo[] = JSON.parse( const pronounsRaw: PronounInfo[] = JSON.parse(
fs.readFileSync(join(dataDirectory, "pronouns.json")).toString() fs.readFileSync(join(dataDirectory, "pronouns.json")).toString()
); );
const licensesRaw: LicenseInfo[] = JSON.parse( const licensesRaw: LicenseInfo[] = JSON.parse(
fs.readFileSync(join(dataDirectory, "licenses.json")).toString() fs.readFileSync(join(dataDirectory, "licenses.json")).toString()
); );
const fullUnicorns: UnicornInfo[] = unicornsRaw.map((unicorn) => { const fullUnicorns: UnicornInfo[] = unicornsRaw.map((unicorn) => {
const absoluteFSPath = join(dataDirectory, unicorn.profileImg); const absoluteFSPath = join(dataDirectory, unicorn.profileImg);
/** /**
* `getFullRelativePath` strips all prefixing `/`, so we must add one manually * `getFullRelativePath` strips all prefixing `/`, so we must add one manually
*/ */
const relativeServerPath = '/' + getFullRelativePath( const relativeServerPath =
"content/data/", "/" + getFullRelativePath("content/data/", unicorn.profileImg);
unicorn.profileImg const profileImgSize = getImageSize(unicorn.profileImg, dataDirectory);
);
const profileImgSize = getImageSize(unicorn.profileImg, dataDirectory);
// Mutation go BRR // Mutation go BRR
const newUnicorn: UnicornInfo = unicorn as never; const newUnicorn: UnicornInfo = unicorn as never;
newUnicorn.profileImgMeta = { newUnicorn.profileImgMeta = {
height: profileImgSize.height as number, height: profileImgSize.height as number,
width: profileImgSize.width as number, width: profileImgSize.width as number,
relativePath: unicorn.profileImg, relativePath: unicorn.profileImg,
relativeServerPath, relativeServerPath,
absoluteFSPath, absoluteFSPath,
}; };
newUnicorn.rolesMeta = unicorn.roles.map( newUnicorn.rolesMeta = unicorn.roles.map(
(role) => rolesRaw.find((rRole) => rRole.id === role)! (role) => rolesRaw.find((rRole) => rRole.id === role)!
); );
newUnicorn.pronounsMeta = pronounsRaw.find( newUnicorn.pronounsMeta = pronounsRaw.find(
(proWithNouns) => proWithNouns.id === unicorn.pronouns (proWithNouns) => proWithNouns.id === unicorn.pronouns
)!; )!;
return newUnicorn; return newUnicorn;
}); });
export { export {
fullUnicorns as unicorns, fullUnicorns as unicorns,
rolesRaw as roles, rolesRaw as roles,
pronounsRaw as pronouns, pronounsRaw as pronouns,
licensesRaw as licenses, licensesRaw as licenses,
}; };

View File

@@ -12,37 +12,37 @@ import * as fs from "fs";
import * as path from "path"; import * as path from "path";
const getIndexPath = (lang: Languages) => { const getIndexPath = (lang: Languages) => {
const indexPath = lang !== "en" ? `index.${lang}.md` : `index.md`; const indexPath = lang !== "en" ? `index.${lang}.md` : `index.md`;
return indexPath; return indexPath;
}; };
export function getPostSlugs(lang: Languages) { export function getPostSlugs(lang: Languages) {
// Avoid errors trying to read from `.DS_Store` files // Avoid errors trying to read from `.DS_Store` files
return fs return fs
.readdirSync(postsDirectory) .readdirSync(postsDirectory)
.filter(isNotJunk) .filter(isNotJunk)
.filter((dir) => .filter((dir) =>
fs.existsSync(path.resolve(postsDirectory, dir, getIndexPath(lang))) fs.existsSync(path.resolve(postsDirectory, dir, getIndexPath(lang)))
); );
} }
export const getAllPosts = (lang: Languages): PostInfo[] => { export const getAllPosts = (lang: Languages): PostInfo[] => {
const slugs = getPostSlugs(lang); const slugs = getPostSlugs(lang);
return slugs.map(slug => { return slugs.map((slug) => {
const file = { const file = {
path: path.join(postsDirectory, slug, getIndexPath(lang)), path: path.join(postsDirectory, slug, getIndexPath(lang)),
data: { data: {
astro: { astro: {
frontmatter: {}, frontmatter: {},
}, },
}, },
}; };
(rehypeUnicornPopulatePost as any)()(undefined, file); (rehypeUnicornPopulatePost as any)()(undefined, file);
return { return {
...(file.data.astro.frontmatter as any || {}).frontmatterBackup, ...((file.data.astro.frontmatter as any) || {}).frontmatterBackup,
...file.data.astro.frontmatter ...file.data.astro.frontmatter,
}; };
}) });
} };

View File

@@ -4,14 +4,14 @@ import sizeOf from "image-size";
const absolutePathRegex = /^(?:[a-z]+:)?\/\//; const absolutePathRegex = /^(?:[a-z]+:)?\/\//;
export function getImageSize(src, dir) { export function getImageSize(src, dir) {
if (absolutePathRegex.exec(src)) { if (absolutePathRegex.exec(src)) {
return; return;
} }
// Treat `/` as a relative path, according to the server // Treat `/` as a relative path, according to the server
const shouldJoin = !path.isAbsolute(src) || src.startsWith("/"); const shouldJoin = !path.isAbsolute(src) || src.startsWith("/");
if (dir && shouldJoin) { if (dir && shouldJoin) {
src = path.join(dir, src); src = path.join(dir, src);
} }
return sizeOf(src); return sizeOf(src);
} }

View File

@@ -4,26 +4,26 @@ import { getAllPosts } from "./get-all-posts";
const postLangMap = new Map<string, ReturnType<typeof getAllPostsByLang>>(); const postLangMap = new Map<string, ReturnType<typeof getAllPostsByLang>>();
const getAllPostsByLang = ( const getAllPostsByLang = (
lang: Languages lang: Languages
): { suggestedPosts: PostInfo[]; dateSorted: PostInfo[] } => { ): { suggestedPosts: PostInfo[]; dateSorted: PostInfo[] } => {
if (postLangMap.has(lang)) return postLangMap.get(lang)!; if (postLangMap.has(lang)) return postLangMap.get(lang)!;
const suggestedPosts = getAllPosts(lang); const suggestedPosts = getAllPosts(lang);
// We must spread, since `sort` mutates the original array // We must spread, since `sort` mutates the original array
const dateSorted = [...suggestedPosts].sort((postA, postB) => { const dateSorted = [...suggestedPosts].sort((postA, postB) => {
return ( return (
// Newest first // Newest first
new Date(postB.published) < new Date(postA.published) ? -1 : 1 new Date(postB.published) < new Date(postA.published) ? -1 : 1
); );
}); });
postLangMap.set(lang, { suggestedPosts, dateSorted }); postLangMap.set(lang, { suggestedPosts, dateSorted });
return { suggestedPosts, dateSorted }; return { suggestedPosts, dateSorted };
}; };
export type OrderSuggestPosts = ReturnType< export type OrderSuggestPosts = ReturnType<
typeof getAllPostsByLang typeof getAllPostsByLang
>["suggestedPosts"]; >["suggestedPosts"];
/** /**
@@ -55,130 +55,127 @@ export type OrderSuggestPosts = ReturnType<
* 1, 4, 5 * 1, 4, 5
*/ */
const howManySimilarBetween = <T>(arr1: T[], arr2: T[]): number => { const howManySimilarBetween = <T>(arr1: T[], arr2: T[]): number => {
let match = 0; let match = 0;
for (let item of arr1) { for (const item of arr1) {
if (arr2.includes(item)) match++; if (arr2.includes(item)) match++;
} }
return match; return match;
}; };
const getOrderRange = (arr: OrderSuggestPosts) => { const getOrderRange = (arr: OrderSuggestPosts) => {
return arr.reduce<{ return arr.reduce<{
largest: null | OrderSuggestPosts[number]; largest: null | OrderSuggestPosts[number];
smallest: null | OrderSuggestPosts[number]; smallest: null | OrderSuggestPosts[number];
}>( }>(
(prev, curr) => { (prev, curr) => {
if (prev.smallest === null || prev.largest === null) { if (prev.smallest === null || prev.largest === null) {
return { return {
largest: curr, largest: curr,
smallest: curr, smallest: curr,
}; };
} }
if (curr.order! < prev.smallest.order!) { if (curr.order! < prev.smallest.order!) {
prev.smallest = curr; prev.smallest = curr;
} }
if (curr.order! > prev.largest.order!) { if (curr.order! > prev.largest.order!) {
prev.largest = curr; prev.largest = curr;
} }
return prev; return prev;
}, },
{ largest: null, smallest: null } { largest: null, smallest: null }
) as never as { ) as never as {
largest: OrderSuggestPosts[number]; largest: OrderSuggestPosts[number];
smallest: OrderSuggestPosts[number]; smallest: OrderSuggestPosts[number];
}; };
}; };
export const getSuggestedArticles = ( export const getSuggestedArticles = (postNode: PostInfo, lang: Languages) => {
postNode: PostInfo, const { suggestedPosts, dateSorted } = getAllPostsByLang(lang);
lang: Languages
) => {
const { suggestedPosts, dateSorted } = getAllPostsByLang(lang);
let extraSuggestedArticles: OrderSuggestPosts = []; const extraSuggestedArticles: OrderSuggestPosts = [];
let suggestedArticles: OrderSuggestPosts = []; const suggestedArticles: OrderSuggestPosts = [];
let similarTags: Array<{ const similarTags: Array<{
post: OrderSuggestPosts[number]; post: OrderSuggestPosts[number];
howManyTagsSimilar: number; howManyTagsSimilar: number;
}> = []; }> = [];
for (let post of suggestedPosts) { for (const post of suggestedPosts) {
// Early "return" for value // Early "return" for value
if (suggestedArticles.length >= 3) break; if (suggestedArticles.length >= 3) break;
// Don't return the same article // Don't return the same article
if (post.slug === postNode.slug) continue; if (post.slug === postNode.slug) continue;
if (!!post.series && post.series === postNode.series) { if (!!post.series && post.series === postNode.series) {
const { largest, smallest } = const { largest, smallest } =
getOrderRange([...suggestedArticles, postNode]) || {}; getOrderRange([...suggestedArticles, postNode]) || {};
let newArticlePushed = false; let newArticlePushed = false;
if ( if (
largest && largest &&
smallest && smallest &&
(post.order === smallest.order! - 1 || (post.order === smallest.order! - 1 ||
post.order === largest.order! + 1) post.order === largest.order! + 1)
) { ) {
suggestedArticles.push(post); suggestedArticles.push(post);
newArticlePushed = false; newArticlePushed = false;
} }
/** /**
* Because we've just updated the `largest` and `smallest`, it's possible * Because we've just updated the `largest` and `smallest`, it's possible
* there's another match in our list of suggested articles. Go check * there's another match in our list of suggested articles. Go check
* *
* This may seem bad to do a while loop here, but I promise that we'll * This may seem bad to do a while loop here, but I promise that we'll
* never have a series longer than even, like, 20 articles. This is a massive * never have a series longer than even, like, 20 articles. This is a massive
* improvement over looping through the entire list of articles. * improvement over looping through the entire list of articles.
*/ */
while (newArticlePushed) { while (newArticlePushed) {
if (suggestedArticles.length >= 3) break; if (suggestedArticles.length >= 3) break;
if (extraSuggestedArticles.length === 0) break; if (extraSuggestedArticles.length === 0) break;
const { largest, smallest } = getOrderRange(suggestedArticles) || {}; const { largest, smallest } = getOrderRange(suggestedArticles) || {};
for (let suggestedPost of extraSuggestedArticles) { for (const suggestedPost of extraSuggestedArticles) {
if ( if (
suggestedPost.order === smallest.order! - 1 || suggestedPost.order === smallest.order! - 1 ||
suggestedPost.order === largest.order! + 1 suggestedPost.order === largest.order! + 1
) { ) {
suggestedArticles.push(suggestedPost); suggestedArticles.push(suggestedPost);
} }
} }
} }
if (suggestedArticles.length >= 3) break; if (suggestedArticles.length >= 3) break;
extraSuggestedArticles.push(post); extraSuggestedArticles.push(post);
} }
const howManyTagsSimilar = howManySimilarBetween( const howManyTagsSimilar = howManySimilarBetween(
post.tags, post.tags,
postNode.tags || [] postNode.tags || []
); );
if (howManyTagsSimilar > 0) { if (howManyTagsSimilar > 0) {
similarTags.push({ post, howManyTagsSimilar }); similarTags.push({ post, howManyTagsSimilar });
} }
} }
// Check to see if there are at least three suggested articles. // Check to see if there are at least three suggested articles.
// If not, fill it with another array of suggested articles. // If not, fill it with another array of suggested articles.
const fillSuggestionArrayWith = (otherArr: OrderSuggestPosts) => { const fillSuggestionArrayWith = (otherArr: OrderSuggestPosts) => {
if (suggestedArticles.length < 3) { if (suggestedArticles.length < 3) {
let sizeToPush = 3 - suggestedArticles.length; let sizeToPush = 3 - suggestedArticles.length;
for (const item of otherArr) { for (const item of otherArr) {
// Handle non-blog content, like about page // Handle non-blog content, like about page
if (!item?.published) continue; if (!item?.published) continue;
// Don't suggest itself // Don't suggest itself
if (item.slug === postNode.slug) continue; if (item.slug === postNode.slug) continue;
// No duplicates, please! // No duplicates, please!
if (suggestedArticles.includes(item)) continue; if (suggestedArticles.includes(item)) continue;
suggestedArticles.push(item); suggestedArticles.push(item);
sizeToPush--; sizeToPush--;
if (sizeToPush <= 0) return; if (sizeToPush <= 0) return;
} }
} }
}; };
const tagSimilaritySorted = similarTags const tagSimilaritySorted = similarTags
.sort((a, b) => b.howManyTagsSimilar - a.howManyTagsSimilar) .sort((a, b) => b.howManyTagsSimilar - a.howManyTagsSimilar)
.map(({ post }) => post); .map(({ post }) => post);
fillSuggestionArrayWith(tagSimilaritySorted); fillSuggestionArrayWith(tagSimilaritySorted);
fillSuggestionArrayWith(dateSorted); fillSuggestionArrayWith(dateSorted);
return suggestedArticles; return suggestedArticles;
}; };

View File

@@ -10,68 +10,72 @@ import path from "path";
*/ */
import { getImage } from "../../../node_modules/@astrojs/image"; import { getImage } from "../../../node_modules/@astrojs/image";
import sharp_service from "../../../node_modules/@astrojs/image/dist/loaders/sharp.js"; import sharp_service from "../../../node_modules/@astrojs/image/dist/loaders/sharp.js";
import {getImageSize} from "../get-image-size"; import { getImageSize } from "../get-image-size";
import {fileURLToPath} from "url"; import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
interface RehypeAstroImageProps { interface RehypeAstroImageProps {
maxHeight?: number; maxHeight?: number;
maxWidth?: number; maxWidth?: number;
} }
export const rehypeAstroImageMd: Plugin< export const rehypeAstroImageMd: Plugin<
[RehypeAstroImageProps | never], [RehypeAstroImageProps | never],
Root Root
> = ({ maxHeight, maxWidth }) => { > = ({ maxHeight, maxWidth }) => {
return async (tree, file) => { return async (tree, file) => {
// HACK: This is a hack that heavily relies on `getImage`'s internals :( // HACK: This is a hack that heavily relies on `getImage`'s internals :(
globalThis.astroImage = { globalThis.astroImage = {
loader: sharp_service ?? globalThis.astroImage?.loader, loader: sharp_service ?? globalThis.astroImage?.loader,
}; };
let imgNodes: any[] = []; const imgNodes: any[] = [];
visit(tree, (node: any) => { visit(tree, (node: any) => {
if (node.tagName === "img") { if (node.tagName === "img") {
imgNodes.push(node); imgNodes.push(node);
} }
}); });
await Promise.all( await Promise.all(
imgNodes.map(async (node) => { imgNodes.map(async (node) => {
const slug = path.dirname(file.path).split('/').at(-1); const slug = path.dirname(file.path).split("/").at(-1);
const filePathDir = path.resolve(__dirname, '../../../public/content/blog', slug) const filePathDir = path.resolve(
__dirname,
"../../../public/content/blog",
slug
);
// TODO: How should remote images be handled? // TODO: How should remote images be handled?
const dimensions = getImageSize(node.properties.src, filePathDir) || { const dimensions = getImageSize(node.properties.src, filePathDir) || {
height: undefined, height: undefined,
width: undefined, width: undefined,
}; };
// TODO: Remote images? // TODO: Remote images?
if (!dimensions.height || !dimensions.width) return; if (!dimensions.height || !dimensions.width) return;
const imgRatioHeight = dimensions.height / dimensions.width; const imgRatioHeight = dimensions.height / dimensions.width;
const imgRatioWidth = dimensions.width / dimensions.height; const imgRatioWidth = dimensions.width / dimensions.height;
if (maxHeight && dimensions.height > maxHeight) { if (maxHeight && dimensions.height > maxHeight) {
dimensions.height = maxHeight; dimensions.height = maxHeight;
dimensions.width = maxHeight * imgRatioWidth; dimensions.width = maxHeight * imgRatioWidth;
} }
if (maxWidth && dimensions.width > maxWidth) { if (maxWidth && dimensions.width > maxWidth) {
dimensions.width = maxWidth; dimensions.width = maxWidth;
dimensions.height = maxWidth * imgRatioHeight; dimensions.height = maxWidth * imgRatioHeight;
} }
const imgProps = await getImage({ const imgProps = await getImage({
src: `/content/blog/${slug}/${node.properties.src}`, src: `/content/blog/${slug}/${node.properties.src}`,
height: dimensions.height, height: dimensions.height,
width: dimensions.width, width: dimensions.width,
}); });
node.properties.src = imgProps.src; node.properties.src = imgProps.src;
}) })
); );
}; };
}; };

View File

@@ -1,37 +1,37 @@
import { Root } from "hast"; import { Root } from "hast";
import { Plugin } from "unified"; import { Plugin } from "unified";
import {visit} from 'unist-util-visit' import { visit } from "unist-util-visit";
interface RehypeExcerptProps { interface RehypeExcerptProps {
maxLength: number; maxLength: number;
} }
export const rehypeExcerpt: Plugin< export const rehypeExcerpt: Plugin<[RehypeExcerptProps | never], Root> = ({
[RehypeExcerptProps | never], maxLength,
Root }) => {
> = ({maxLength}) => { return (tree, file) => {
return (tree, file) => { const getFileExcerpt = () =>
const getFileExcerpt = () => (file?.data?.astro as any)?.frontmatter?.excerpt as string; (file?.data?.astro as any)?.frontmatter?.excerpt as string;
const setFileExcerpt = (val) => { const setFileExcerpt = (val) => {
(file.data.astro as any).frontmatter.excerpt = val; (file.data.astro as any).frontmatter.excerpt = val;
} };
if (!getFileExcerpt()) { if (!getFileExcerpt()) {
setFileExcerpt(""); setFileExcerpt("");
} }
visit(tree, 'element', node => { visit(tree, "element", (node) => {
const fileExcerpt = getFileExcerpt(); const fileExcerpt = getFileExcerpt();
if (fileExcerpt.length >= maxLength) return; if (fileExcerpt.length >= maxLength) return;
// Don't get headers or anything other than text // Don't get headers or anything other than text
if (node.tagName === 'p') { if (node.tagName === "p") {
visit(node, 'text', textNode => { visit(node, "text", (textNode) => {
let newVal = fileExcerpt + textNode.value; let newVal = fileExcerpt + textNode.value;
if (newVal.length > maxLength) { if (newVal.length > maxLength) {
newVal = newVal.slice(0, maxLength - 3) + "..."; newVal = newVal.slice(0, maxLength - 3) + "...";
} }
setFileExcerpt(newVal); setFileExcerpt(newVal);
}) });
} }
}); });
}; };
}; };

View File

@@ -2,35 +2,35 @@ import { headingRank } from "hast-util-heading-rank";
import { hasProperty } from "hast-util-has-property"; import { hasProperty } from "hast-util-has-property";
import { toString } from "hast-util-to-string"; import { toString } from "hast-util-to-string";
import { Root, Parent } from "hast"; import { Root, Parent } from "hast";
import {visit} from "unist-util-visit"; import { visit } from "unist-util-visit";
/** /**
* Plugin to add `data-header-text`s to headings. * Plugin to add `data-header-text`s to headings.
*/ */
export const rehypeHeaderText = () => { export const rehypeHeaderText = () => {
return (tree: Root, file) => { return (tree: Root, file) => {
visit(tree, "element", (node: Parent["children"][number]) => { visit(tree, "element", (node: Parent["children"][number]) => {
if ( if (
headingRank(node) && headingRank(node) &&
"properties" in node && "properties" in node &&
node.properties && node.properties &&
!hasProperty(node, "data-header-text") !hasProperty(node, "data-header-text")
) { ) {
const headerText = toString(node); const headerText = toString(node);
node.properties["data-header-text"] = headerText; node.properties["data-header-text"] = headerText;
const headingWithID = { const headingWithID = {
value: headerText, value: headerText,
depth: headingRank(node)!, depth: headingRank(node)!,
slug: node.properties["id"] as string, slug: node.properties["id"] as string,
}; };
if (file.data.astro.frontmatter.headingsWithId) { if (file.data.astro.frontmatter.headingsWithId) {
file.data.astro.frontmatter.headingsWithId.push(headingWithID) file.data.astro.frontmatter.headingsWithId.push(headingWithID);
} else { } else {
file.data.astro.frontmatter.headingsWithId = [headingWithID]; file.data.astro.frontmatter.headingsWithId = [headingWithID];
} }
} }
}); });
}; };
}; };

View File

@@ -9,83 +9,84 @@ import { fromHtml } from "hast-util-from-html";
import path from "path"; import path from "path";
interface RehypeUnicornElementMapProps { interface RehypeUnicornElementMapProps {}
}
function escapeHTML(s) { function escapeHTML(s) {
if (!s) return s; if (!s) return s;
return s return s
.replace(/&/g, "&amp;") .replace(/&/g, "&amp;")
.replace(/"/g, "&quot;") .replace(/"/g, "&quot;")
.replace(/</g, "&lt;") .replace(/</g, "&lt;")
.replace(/>/g, "&gt;"); .replace(/>/g, "&gt;");
} }
// TODO: Add switch/case and dedicated files ala "Components" // TODO: Add switch/case and dedicated files ala "Components"
export const rehypeUnicornElementMap: Plugin< export const rehypeUnicornElementMap: Plugin<
[RehypeUnicornElementMapProps | never], [RehypeUnicornElementMapProps | never],
Root Root
> = () => { > = () => {
return async (tree, file) => { return async (tree, file) => {
visit(tree, (node: any) => { visit(tree, (node: any) => {
if (node.tagName === "iframe") { if (node.tagName === "iframe") {
node.properties.width ??= EMBED_SIZE.w; node.properties.width ??= EMBED_SIZE.w;
node.properties.height ??= EMBED_SIZE.h; node.properties.height ??= EMBED_SIZE.h;
node.properties.loading ??= "lazy"; node.properties.loading ??= "lazy";
} }
if (node.tagName === "video") { if (node.tagName === "video") {
node.properties.muted ??= true; node.properties.muted ??= true;
node.properties.autoPlay ??= true; node.properties.autoPlay ??= true;
node.properties.controls ??= true; node.properties.controls ??= true;
node.properties.loop ??= true; node.properties.loop ??= true;
node.properties.width ??= "100%"; node.properties.width ??= "100%";
node.properties.height ??= "auto"; node.properties.height ??= "auto";
} }
if (node.tagName === "a") { if (node.tagName === "a") {
const href = node.properties.href; const href = node.properties.href;
const isInternalLink = isRelativePath(href || ""); const isInternalLink = isRelativePath(href || "");
if (!isInternalLink) { if (!isInternalLink) {
node.properties.target = "_blank"; node.properties.target = "_blank";
node.properties.rel = "nofollow noopener noreferrer"; node.properties.rel = "nofollow noopener noreferrer";
} }
} }
if (node.tagName === "table" && !node.properties["has-changed"]) { if (node.tagName === "table" && !node.properties["has-changed"]) {
const children = [...node.children]; const children = [...node.children];
const properties = { ...node.properties, "has-changed": true }; const properties = { ...node.properties, "has-changed": true };
node.tagName = "div"; node.tagName = "div";
node.properties = { node.properties = {
class: "table-container", class: "table-container",
}; };
node.children = [ node.children = [
{ {
tagName: "table", tagName: "table",
type: "element", type: "element",
children, children,
properties, properties,
}, },
]; ];
} }
if ( if (
node.tagName === "h1" || node.tagName === "h1" ||
node.tagName === "h2" || node.tagName === "h2" ||
node.tagName === "h3" || node.tagName === "h3" ||
node.tagName === "h4" || node.tagName === "h4" ||
node.tagName === "h5" || node.tagName === "h5" ||
node.tagName === "h6" node.tagName === "h6"
) { ) {
const id = node.properties.id; const id = node.properties.id;
const headerText = node.properties["data-header-text"]; const headerText = node.properties["data-header-text"];
node.properties.style = node.properties.style =
(node.properties.style || "") + "position: relative;"; (node.properties.style || "") + "position: relative;";
const headerLinkHTML = ` const headerLinkHTML = `
<a <a
href="#${id}" href="#${id}"
aria-label="Permalink for &quot;${escapeHTML(headerText)}&quot;" aria-label="Permalink for &quot;${escapeHTML(
headerText
)}&quot;"
class="anchor before" class="anchor before"
> >
<svg <svg
@@ -107,9 +108,9 @@ export const rehypeUnicornElementMap: Plugin<
</a> </a>
`; `;
const hastHeader = fromHtml(headerLinkHTML, { fragment: true }); const hastHeader = fromHtml(headerLinkHTML, { fragment: true });
node.children = [hastHeader, ...node.children]; node.children = [hastHeader, ...node.children];
} }
}); });
}; };
}; };

View File

@@ -1,25 +1,24 @@
import { Root } from "hast"; import { Root } from "hast";
import { Plugin } from "unified"; import { Plugin } from "unified";
import {getSuggestedArticles} from "../get-suggested-articles"; import { getSuggestedArticles } from "../get-suggested-articles";
interface RehypeUnicornGetSuggestedPostsProps { interface RehypeUnicornGetSuggestedPostsProps {}
}
export const rehypeUnicornGetSuggestedPosts: Plugin< export const rehypeUnicornGetSuggestedPosts: Plugin<
[RehypeUnicornGetSuggestedPostsProps | never], [RehypeUnicornGetSuggestedPostsProps | never],
Root Root
> = () => { > = () => {
return (_, file) => { return (_, file) => {
function setData(key: string, val: any) { function setData(key: string, val: any) {
(file.data.astro as any).frontmatter[key] = val; (file.data.astro as any).frontmatter[key] = val;
} }
const post = { const post = {
...(file.data.astro as any).frontmatter.frontmatterBackup, ...(file.data.astro as any).frontmatter.frontmatterBackup,
...(file.data.astro as any).frontmatter ...(file.data.astro as any).frontmatter,
} };
const suggestedArticles = getSuggestedArticles(post, 'en'); const suggestedArticles = getSuggestedArticles(post, "en");
setData("suggestedArticles", suggestedArticles); setData("suggestedArticles", suggestedArticles);
}; };
}; };

View File

@@ -5,77 +5,73 @@ import { readFileSync } from "fs";
import * as path from "path"; import * as path from "path";
import { licenses, unicorns } from "../data"; import { licenses, unicorns } from "../data";
interface RehypeUnicornPopulatePostProps { interface RehypeUnicornPopulatePostProps {}
}
export const rehypeUnicornPopulatePost: Plugin< export const rehypeUnicornPopulatePost: Plugin<
[RehypeUnicornPopulatePostProps | never], [RehypeUnicornPopulatePostProps | never],
Root Root
> = () => { > = () => {
return (_, file) => { return (_, file) => {
function setData(key: string, val: any) {
(file.data.astro as any).frontmatter[key] = val;
}
function setData(key: string, val: any) { const fileContents = readFileSync(file.path, "utf8");
(file.data.astro as any).frontmatter[key] = val; const { data: frontmatter } = matter(fileContents);
}
const fileContents = readFileSync(file.path, "utf8"); const directorySplit = file.path.split(path.sep);
const { data: frontmatter } = matter(fileContents);
const directorySplit = file.path.split(path.sep); // This is the folder name, AKA how we generate the slug ID
const slug = directorySplit.at(-2);
// This is the folder name, AKA how we generate the slug ID // Calculate post locale
const slug = directorySplit.at(-2); // index.md or index.es.md
const indexName = directorySplit.at(-1);
const indexSplit = indexName.split(".");
let locale = indexSplit.at(-2);
if (locale === "index") {
locale = "en";
}
// Calculate post locale // // TODO: Add translations
// index.md or index.es.md // if (fields.translations) {
const indexName = directorySplit.at(-1); // const langsToQuery: Languages[] = Object.keys(languages).filter(
const indexSplit = indexName.split('.'); // (l) => l !== lang
let locale = indexSplit.at(-2); // ) as never;
if (locale === 'index') { // pickedData.translations = langsToQuery
locale = 'en'; // .filter((lang) =>
} // fs.existsSync(resolve(dirname(fullPath), getIndexPath(lang)))
// )
// .reduce((prev, lang) => {
// prev[lang] = languages[lang];
// return prev;
// }, {} as Record<Languages, string>);
// }
// // TODO: Add translations // // TODO: Add collection slug
// if (fields.translations) { // if (fields.collectionSlug) {
// const langsToQuery: Languages[] = Object.keys(languages).filter( // if (frontmatterData.series) {
// (l) => l !== lang // pickedData.collectionSlug = collectionsByName.find(
// ) as never; // (collection) => collection.associatedSeries === frontmatterData.series
// pickedData.translations = langsToQuery // )?.slug;
// .filter((lang) => // }
// fs.existsSync(resolve(dirname(fullPath), getIndexPath(lang))) // if (!pickedData.collectionSlug) pickedData.collectionSlug = null;
// ) // }
// .reduce((prev, lang) => {
// prev[lang] = languages[lang];
// return prev;
// }, {} as Record<Languages, string>);
// }
// // TODO: Add collection slug const authorsMeta = (frontmatter.authors as string[]).map(
// if (fields.collectionSlug) { (author) => unicorns.find((unicorn) => unicorn.id === author)!
// if (frontmatterData.series) { );
// pickedData.collectionSlug = collectionsByName.find(
// (collection) => collection.associatedSeries === frontmatterData.series
// )?.slug;
// }
// if (!pickedData.collectionSlug) pickedData.collectionSlug = null;
// }
const authorsMeta = (frontmatter.authors as string[]).map( let license;
(author) => unicorns.find((unicorn) => unicorn.id === author)! if (frontmatter.license) {
); license = licenses.find((l) => l.id === frontmatter.license);
}
if (!license) license = null;
let license; setData("slug", slug);
if (frontmatter.license) { setData("locale", locale);
license = licenses.find( setData("authorsMeta", authorsMeta);
(l) => l.id === frontmatter.license setData("license", license);
); setData("frontmatterBackup", frontmatter);
} };
if (!license) license = null;
setData('slug', slug);
setData('locale', locale);
setData('authorsMeta', authorsMeta);
setData('license', license);
setData('frontmatterBackup', frontmatter);
};
}; };

View File

@@ -27,46 +27,50 @@ import { visit } from "unist-util-visit";
import { unified } from "unified"; import { unified } from "unified";
import english from "retext-english"; import english from "retext-english";
import rehypeRetext from 'rehype-retext'; import rehypeRetext from "rehype-retext";
import { validateConfig } from "astro/dist/types/core/config"; import { validateConfig } from "astro/dist/types/core/config";
interface RemarkCountProps {} interface RemarkCountProps {}
function count(counts: Record<string, number>) { function count(counts: Record<string, number>) {
return () => counter; return () => counter;
function counter(tree: Root) { function counter(tree: Root) {
visit(tree, visitor); visit(tree, visitor);
function visitor(node: Node) { function visitor(node: Node) {
if (node.type === 'SourceNode') { if (node.type === "SourceNode") {
const inlineCount = (node as never as {value: string}).value.split(/\b/g).length; const inlineCount = (node as never as { value: string }).value.split(
counts["InlineCodeWords"] = (counts["InlineCodeWords"] || 0) + inlineCount; /\b/g
} ).length;
counts[node.type] = (counts[node.type] || 0) + 1; counts["InlineCodeWords"] =
} (counts["InlineCodeWords"] || 0) + inlineCount;
} }
counts[node.type] = (counts[node.type] || 0) + 1;
}
}
} }
export const rehypeWordCount: Plugin<[RemarkCountProps | never], Root> = () => { export const rehypeWordCount: Plugin<[RemarkCountProps | never], Root> = () => {
return async (tree, file) => { return async (tree, file) => {
const counts = {} as { const counts = {} as {
InlineCodeWords: number; InlineCodeWords: number;
RootNode: number; RootNode: number;
ParagraphNode: number; ParagraphNode: number;
SentenceNode: number; SentenceNode: number;
WordNode: number; WordNode: number;
TextNode: number; TextNode: number;
WhiteSpaceNode: number; WhiteSpaceNode: number;
PunctuationNode: number; PunctuationNode: number;
SymbolNode: number; SymbolNode: number;
SourceNode: number; SourceNode: number;
}; };
await unified() await unified()
.use(rehypeRetext, unified().use(english).use(count(counts))) .use(rehypeRetext, unified().use(english).use(count(counts)))
.run(tree); .run(tree);
(file.data.astro as any).frontmatter.wordCount = (counts.InlineCodeWords || 0) + (counts.TextNode || 0); (file.data.astro as any).frontmatter.wordCount =
}; (counts.InlineCodeWords || 0) + (counts.TextNode || 0);
};
}; };

View File

@@ -1 +1 @@
export * from './tabs'; export * from "./tabs";

View File

@@ -5,29 +5,29 @@ import { Plugin } from "unified";
import { getHeaderNodeId, slugs } from "rehype-slug-custom-id"; import { getHeaderNodeId, slugs } from "rehype-slug-custom-id";
interface ElementNode extends Parent { interface ElementNode extends Parent {
tagName: string; tagName: string;
properties: any; properties: any;
} }
const isNodeHeading = (n: ElementNode) => const isNodeHeading = (n: ElementNode) =>
n.type === "element" && /h[1-6]/.exec(n.tagName); n.type === "element" && /h[1-6]/.exec(n.tagName);
const findLargestHeading = (nodes: ElementNode[]) => { const findLargestHeading = (nodes: ElementNode[]) => {
let largestSize = Infinity; let largestSize = Infinity;
for (let node of nodes) { for (const node of nodes) {
if (!isNodeHeading(node)) continue; if (!isNodeHeading(node)) continue;
const size = parseInt(node.tagName.substr(1), 10); const size = parseInt(node.tagName.substr(1), 10);
largestSize = Math.min(largestSize, size); largestSize = Math.min(largestSize, size);
} }
return largestSize; return largestSize;
}; };
const isNodeLargestHeading = (n: ElementNode, largestSize: number) => const isNodeLargestHeading = (n: ElementNode, largestSize: number) =>
isNodeHeading(n) && parseInt(n.tagName.substr(1), 10) === largestSize; isNodeHeading(n) && parseInt(n.tagName.substr(1), 10) === largestSize;
export interface RehypeTabsProps { export interface RehypeTabsProps {
injectSubheaderProps?: boolean; injectSubheaderProps?: boolean;
tabSlugifyProps?: Parameters<typeof getHeaderNodeId>[1]; tabSlugifyProps?: Parameters<typeof getHeaderNodeId>[1];
} }
/** /**
@@ -48,127 +48,127 @@ export interface RehypeTabsProps {
* @see https://github.com/reactjs/react-tabs * @see https://github.com/reactjs/react-tabs
*/ */
export const rehypeTabs: Plugin<[RehypeTabsProps | never], Root> = ({ export const rehypeTabs: Plugin<[RehypeTabsProps | never], Root> = ({
injectSubheaderProps = false, injectSubheaderProps = false,
tabSlugifyProps = {}, tabSlugifyProps = {},
}) => { }) => {
return (tree) => { return (tree) => {
const replaceTabNodes = (nodes: Node[]) => { const replaceTabNodes = (nodes: Node[]) => {
let sectionStarted = false; let sectionStarted = false;
const largestSize = findLargestHeading(nodes as ElementNode[]); const largestSize = findLargestHeading(nodes as ElementNode[]);
const tabsContainer = { const tabsContainer = {
type: "element", type: "element",
tagName: "div", tagName: "div",
properties: { properties: {
class: "tabs" class: "tabs",
}, },
children: [ children: [
{ {
type: "element", type: "element",
tagName: "ul", tagName: "ul",
properties: { properties: {
role: 'tablist', role: "tablist",
class: "tabs__tab-list" class: "tabs__tab-list",
}, },
children: [] as ElementNode[], children: [] as ElementNode[],
}, },
], ],
}; };
for (const localNode of nodes as ElementNode[]) { for (const localNode of nodes as ElementNode[]) {
if (!sectionStarted && !isNodeLargestHeading(localNode, largestSize)) { if (!sectionStarted && !isNodeLargestHeading(localNode, largestSize)) {
continue; continue;
} }
sectionStarted = true; sectionStarted = true;
if (isNodeLargestHeading(localNode, largestSize)) { if (isNodeLargestHeading(localNode, largestSize)) {
// Make sure that all tabs labeled "thing" aren't also labeled "thing2" // Make sure that all tabs labeled "thing" aren't also labeled "thing2"
slugs.reset(); slugs.reset();
const { id: headerSlug } = getHeaderNodeId( const { id: headerSlug } = getHeaderNodeId(
localNode, localNode,
tabSlugifyProps tabSlugifyProps
); );
// - 1 because the tabs are part of the header // - 1 because the tabs are part of the header
const idx = tabsContainer.children.length - 1; const idx = tabsContainer.children.length - 1;
const header = { const header = {
type: "element", type: "element",
tagName: "li", tagName: "li",
children: localNode.children, children: localNode.children,
properties: { properties: {
role: 'tab', role: "tab",
class: "tabs__tab", class: "tabs__tab",
"data-tabname": headerSlug, "data-tabname": headerSlug,
"aria-selected": idx === 0 ? "true" : 'false', "aria-selected": idx === 0 ? "true" : "false",
"aria-controls": `panel-${idx}`, "aria-controls": `panel-${idx}`,
"id": `tab-${idx}`, id: `tab-${idx}`,
tabIndex: idx === 0 ? "0" : "-1" tabIndex: idx === 0 ? "0" : "-1",
}, },
}; };
const contents = { const contents = {
type: "element", type: "element",
tagName: "div", tagName: "div",
children: [], children: [],
properties: { properties: {
id: `panel-${idx}`, id: `panel-${idx}`,
role: "tabpanel", role: "tabpanel",
class: "tabs__tab-panel", class: "tabs__tab-panel",
tabindex: 0, tabindex: 0,
"aria-labelledby": `tab-${idx}`, "aria-labelledby": `tab-${idx}`,
...(idx === 0 ? {} : {hidden: "true"}) ...(idx === 0 ? {} : { hidden: "true" }),
}, },
}; };
tabsContainer.children[0].children.push(header); tabsContainer.children[0].children.push(header);
tabsContainer.children.push(contents); tabsContainer.children.push(contents);
continue; continue;
} }
if (isNodeHeading(localNode) && injectSubheaderProps) { if (isNodeHeading(localNode) && injectSubheaderProps) {
// This is `tagName: tab` // This is `tagName: tab`
const lastTab = const lastTab =
tabsContainer.children[0].children[ tabsContainer.children[0].children[
tabsContainer.children[0].children.length - 1 tabsContainer.children[0].children.length - 1
]; ];
// Store the related tab ID in the attributes of the header // Store the related tab ID in the attributes of the header
localNode.properties["data-tabname"] = localNode.properties["data-tabname"] =
// Get the last tab's `data-tabname` property // Get the last tab's `data-tabname` property
lastTab.properties["data-tabname"]; lastTab.properties["data-tabname"];
// Add header ID to array // Add header ID to array
lastTab.properties["data-headers"] = JSON.stringify( lastTab.properties["data-headers"] = JSON.stringify(
JSON.parse(lastTab.properties["data-headers"] ?? "[]").concat( JSON.parse(lastTab.properties["data-headers"] ?? "[]").concat(
localNode.properties.id localNode.properties.id
) )
); );
} }
// Push into last `tab-panel` // Push into last `tab-panel`
tabsContainer.children[tabsContainer.children.length - 1].children.push( tabsContainer.children[tabsContainer.children.length - 1].children.push(
localNode localNode
); );
} }
return [tabsContainer]; return [tabsContainer];
}; };
replaceAllBetween( replaceAllBetween(
tree, tree,
{ type: "raw", value: "<!-- tabs:start -->" } as never, { type: "raw", value: "<!-- tabs:start -->" } as never,
{ type: "raw", value: "<!-- tabs:end -->" } as never, { type: "raw", value: "<!-- tabs:end -->" } as never,
replaceTabNodes replaceTabNodes
); );
replaceAllBetween( replaceAllBetween(
tree, tree,
{ type: "comment", value: " tabs:start " } as never, { type: "comment", value: " tabs:start " } as never,
{ type: "comment", value: " tabs:end " } as never, { type: "comment", value: " tabs:end " } as never,
replaceTabNodes replaceTabNodes
); );
return tree; return tree;
}; };
}; };

View File

@@ -8,13 +8,13 @@ import { languages } from "constants/index";
* code handles the parsing and converting of translation formats * code handles the parsing and converting of translation formats
*/ */
export function fileToOpenGraphConverter<T extends Languages>( export function fileToOpenGraphConverter<T extends Languages>(
lang: T lang: T
): T extends `${infer Lang}-${infer Region}` ): T extends `${infer Lang}-${infer Region}`
? `${Lang}_${Uppercase<Region>}` ? `${Lang}_${Uppercase<Region>}`
: T { : T {
const splitLang = lang.split("-"); const splitLang = lang.split("-");
if (splitLang.length === 1) return lang as never; if (splitLang.length === 1) return lang as never;
return `${splitLang[0]}_${splitLang[1].toUpperCase()}` as never; return `${splitLang[0]}_${splitLang[1].toUpperCase()}` as never;
} }
/** /**
@@ -30,12 +30,12 @@ export function fileToOpenGraphConverter<T extends Languages>(
* @example "es-es/posts/test" -> "posts/test" * @example "es-es/posts/test" -> "posts/test"
*/ */
export function removePrefixLanguageFromPath(path: string) { export function removePrefixLanguageFromPath(path: string) {
const langs = Object.keys(languages) as Languages[]; const langs = Object.keys(languages) as Languages[];
const matchedLang = langs.find( const matchedLang = langs.find(
(lang) => path.startsWith(lang) || path.startsWith("/" + lang) (lang) => path.startsWith(lang) || path.startsWith("/" + lang)
); );
if (!matchedLang) return path; if (!matchedLang) return path;
if (path.startsWith("/")) return "/" + path.slice(matchedLang.length + 2); if (path.startsWith("/")) return "/" + path.slice(matchedLang.length + 2);
// +1 since "es/path" needs to axe the trailing "/" // +1 since "es/path" needs to axe the trailing "/"
return path.slice(matchedLang.length + 1); return path.slice(matchedLang.length + 1);
} }

View File

@@ -9,33 +9,35 @@ import slash from "slash";
export const absolutePathRegex = /^(?:[a-z]+:)?\/\//; export const absolutePathRegex = /^(?:[a-z]+:)?\/\//;
export const isRelativePath = (str: string) => { export const isRelativePath = (str: string) => {
const isAbsolute = absolutePathRegex.exec(str); const isAbsolute = absolutePathRegex.exec(str);
if (isAbsolute) return false; if (isAbsolute) return false;
return true; return true;
}; };
var pathJoin = function(...pathArr){ const pathJoin = function (...pathArr) {
return pathArr.map(function(path){ return pathArr
if(path[0] === "/"){ .map(function (path) {
path = path.slice(1); if (path[0] === "/") {
} path = path.slice(1);
if (path.startsWith('./')) { }
path = path.slice(2); if (path.startsWith("./")) {
} path = path.slice(2);
if(path[path.length - 1] === "/"){ }
path = path.slice(0, path.length - 1); if (path[path.length - 1] === "/") {
} path = path.slice(0, path.length - 1);
return path; }
}).join("/"); return path;
} })
.join("/");
};
export const getFullRelativePath = (...paths: string[]) => { export const getFullRelativePath = (...paths: string[]) => {
return isRelativePath(paths[paths.length - 1]) return isRelativePath(paths[paths.length - 1])
? slash(pathJoin(...paths)) ? slash(pathJoin(...paths))
: paths[paths.length - 1]; : paths[paths.length - 1];
}; };
export const trimTrailingSlash = (path: string) => { export const trimTrailingSlash = (path: string) => {
if (path.endsWith("/")) return path.slice(0, path.length - 1); if (path.endsWith("/")) return path.slice(0, path.length - 1);
return path; return path;
}; };

View File

@@ -1,30 +1,30 @@
{ {
"compilerOptions": { "compilerOptions": {
// Enable top-level await, and other modern ESM features. // Enable top-level await, and other modern ESM features.
"target": "ESNext", "target": "ESNext",
"module": "ESNext", "module": "ESNext",
// Enable node-style module resolution, for things like npm package imports. // Enable node-style module resolution, for things like npm package imports.
"moduleResolution": "node", "moduleResolution": "node",
// Enable JSON imports. // Enable JSON imports.
"resolveJsonModule": true, "resolveJsonModule": true,
// Enable stricter transpilation for better output. // Enable stricter transpilation for better output.
"isolatedModules": true, "isolatedModules": true,
"esModuleInterop": true, "esModuleInterop": true,
"jsx": "preserve", "jsx": "preserve",
"jsxImportSource": "preact", "jsxImportSource": "preact",
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
// Add type definitions for our Vite runtime. // Add type definitions for our Vite runtime.
"types": ["@astrojs/image/client"], "types": ["@astrojs/image/client"],
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"constants/*": ["./src/constants/*"], "constants/*": ["./src/constants/*"],
"uu-constants": ["./src/constants"], "uu-constants": ["./src/constants"],
"types/*": ["./src/types/*"], "types/*": ["./src/types/*"],
"uu-types": ["./src/types"], "uu-types": ["./src/types"],
"components/*": ["./src/components/*"], "components/*": ["./src/components/*"],
"utils/*": ["./src/utils/*"], "utils/*": ["./src/utils/*"],
"uu-utils": ["./src/utils"], "uu-utils": ["./src/utils"],
"assets/*": ["./src/assets/*"], "assets/*": ["./src/assets/*"]
} }
} }
} }

View File

@@ -1,20 +1,20 @@
{ {
"framework": "astro", "framework": "astro",
"redirects": [ "redirects": [
{ {
"source": "/authors/crutchcorn/", "source": "/authors/crutchcorn/",
"destination": "/unicorns/crutchcorn", "destination": "/unicorns/crutchcorn",
"permanent": true "permanent": true
}, },
{ {
"source": "/unicorns/", "source": "/unicorns/",
"destination": "/about", "destination": "/about",
"permanent": false "permanent": false
}, },
{ {
"source": "/posts/", "source": "/posts/",
"destination": "/", "destination": "/",
"permanent": true "permanent": true
} }
] ]
} }