initial commit
This commit is contained in:
commit
1afe272ca0
120 changed files with 35905 additions and 0 deletions
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
|
@ -0,0 +1,25 @@
|
|||
.bundle
|
||||
.hugo
|
||||
.DS_Store
|
||||
.sass-cache
|
||||
.gist-cache
|
||||
.pygments-cache
|
||||
.env
|
||||
_deploy
|
||||
public/
|
||||
data/
|
||||
static/images/twitter-*
|
||||
dist
|
||||
hugo
|
||||
sass.old
|
||||
source.old
|
||||
source/_site
|
||||
source/_stash
|
||||
source/stylesheets/screen.css
|
||||
vendor
|
||||
node_modules
|
||||
resources/
|
||||
.vercel
|
||||
auth.json
|
||||
content/en/2019-10-20-podroll.markdown
|
||||
static/javascripts/comlink
|
33
.gitlab-ci.yml
Normal file
33
.gitlab-ci.yml
Normal file
|
@ -0,0 +1,33 @@
|
|||
default:
|
||||
image: node:18.17.1
|
||||
|
||||
deploy_preview:
|
||||
stage: deploy
|
||||
except:
|
||||
- master
|
||||
- develop
|
||||
script:
|
||||
- npm install --global vercel
|
||||
- vercel pull --yes --environment=preview --token="${VERCEL_TOKEN}" -S flear
|
||||
- vercel build --token="${VERCEL_TOKEN}" -S flear
|
||||
- vercel deploy --prebuilt --token="${VERCEL_TOKEN}" -S flear
|
||||
|
||||
deploy_development:
|
||||
stage: deploy
|
||||
only:
|
||||
- develop
|
||||
script:
|
||||
- npm install --global vercel
|
||||
- vercel pull --yes --environment=development --token="${VERCEL_TOKEN}" -S flear
|
||||
- vercel build --token="${VERCEL_TOKEN}" -S flear
|
||||
- vercel deploy --prebuilt --token="${VERCEL_TOKEN}" -S flear
|
||||
|
||||
deploy_production:
|
||||
stage: deploy
|
||||
only:
|
||||
- master
|
||||
script:
|
||||
- npm install --global vercel
|
||||
- vercel pull --yes --environment=production --token="${VERCEL_TOKEN}" -S flear
|
||||
- vercel build --prod --token="${VERCEL_TOKEN}" -S flear
|
||||
- vercel deploy --prebuilt --prod --token="${VERCEL_TOKEN}" -S flear
|
0
.hugo_build.lock
Normal file
0
.hugo_build.lock
Normal file
17
.nowignore
Normal file
17
.nowignore
Normal file
|
@ -0,0 +1,17 @@
|
|||
.bundle
|
||||
.hugo
|
||||
.DS_Store
|
||||
.sass-cache
|
||||
.gist-cache
|
||||
.pygments-cache
|
||||
_deploy
|
||||
public
|
||||
dist
|
||||
hugo
|
||||
sass.old
|
||||
source.old
|
||||
source/_site
|
||||
source/_stash
|
||||
source/stylesheets/screen.css
|
||||
vendor
|
||||
node_modules
|
201
LICENSE
Normal file
201
LICENSE
Normal file
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
25
README.md
Normal file
25
README.md
Normal file
|
@ -0,0 +1,25 @@
|
|||
## Generate Activity Pub key
|
||||
|
||||
```bash
|
||||
npm install ts-node typescript '@types/node'
|
||||
./node_modules/.bin/ts-node --esm generateKeys.mts
|
||||
```
|
||||
|
||||
|
||||
## Setup firebase
|
||||
|
||||
Setup the things
|
||||
|
||||
## Setup Vercel
|
||||
|
||||
Setup the things
|
||||
|
||||
## Trigger post deploy
|
||||
|
||||
```
|
||||
curl -G -X POST --data-urlencode token="<token>" https://flear.org/send-note
|
||||
```
|
||||
|
||||
# License
|
||||
|
||||
This project (excluding post content itself) is released under the Apache License v2
|
154
api/activitypub-html/render.ts
Normal file
154
api/activitypub-html/render.ts
Normal file
|
@ -0,0 +1,154 @@
|
|||
import { VercelRequest, VercelResponse } from "@vercel/node";
|
||||
import * as admin from 'firebase-admin';
|
||||
|
||||
const escapeHTML = (str: string): string => str.replace(/[<>'"]/g,
|
||||
tag => {
|
||||
|
||||
const escapedText = {
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
"'": ''',
|
||||
'"': '"'
|
||||
}[tag];
|
||||
|
||||
return escapedText || tag;
|
||||
}
|
||||
);
|
||||
|
||||
const stripHTML = (str: string): string => str.replace(/<[^>]+>/g, '');
|
||||
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
||||
|
||||
if (!admin.default.apps?.length) {
|
||||
admin.default.initializeApp({
|
||||
credential: admin.default.credential.cert({
|
||||
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
|
||||
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
|
||||
privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n')
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
const db = admin.default.firestore();
|
||||
|
||||
export default async function (req: VercelRequest, res: VercelResponse) {
|
||||
const { body, query, method, headers } = req;
|
||||
|
||||
if (method != "GET") res.status(404).end("Not found");
|
||||
|
||||
res.statusCode = 200;
|
||||
res.setHeader("Content-Type", `text/html`);
|
||||
|
||||
const url: string = (query.url instanceof Array) ? query.url[0] : query.url;
|
||||
const idAsUrl = url.replace(/\//g, "_");
|
||||
|
||||
if (url == null) {
|
||||
res.status(404).end("Not found");
|
||||
return;
|
||||
}
|
||||
|
||||
const likes = await db.collection("likes").doc(idAsUrl).collection("messages");
|
||||
const announces = await db.collection("announces").doc(idAsUrl).collection("messages");
|
||||
const replies = await db.collection("replies").doc(idAsUrl).collection("messages");
|
||||
|
||||
const likesSnapshot = await likes.get();
|
||||
const announcesSnapshot = await announces.get();
|
||||
const repliesSnapshot = await replies.get();
|
||||
|
||||
const likesCount = likesSnapshot.size;
|
||||
const announcesCount = announcesSnapshot.size;
|
||||
const repliesCount = repliesSnapshot.size;
|
||||
|
||||
res.end(`<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Interactions from around the fediverse with ${escapeHTML(url)}</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<style>
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
color: #fefefe;
|
||||
background-color: #212529;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #1bcba2
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #7ad857
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Helvetica,Arial,sans-serif;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
section {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
a img.profile {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50% 50%;
|
||||
}
|
||||
|
||||
ul li {
|
||||
list-style: none;
|
||||
float: left;
|
||||
}
|
||||
|
||||
div.reply p a {
|
||||
margin-right: 1em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Interactions from around the fediverse</h1>
|
||||
<section class="likes">
|
||||
<h4>Likes (${likesCount})</h4>
|
||||
${likesSnapshot.docs.map(doc => {
|
||||
const { actor } = doc.data();
|
||||
if (typeof actor == "string") {
|
||||
return `<a href="${escapeHTML(actor)}" target="_parent" rel="nofollow">${escapeHTML(actor)}</a>`;
|
||||
}
|
||||
|
||||
return `<a title="${escapeHTML(actor.name)}" href="${escapeHTML(actor.url)}" target="_parent" rel="nofollow"><img class="profile" src="${escapeHTML(actor.icon.url)}" alt="The profile picture of ${escapeHTML(actor.name)}"></a>`
|
||||
}
|
||||
).join("")}
|
||||
</section>
|
||||
<section class="announces">
|
||||
<h4>Announces (${announcesCount})</h4>
|
||||
${announcesSnapshot.docs.map(doc => {
|
||||
const { actor } = doc.data();
|
||||
if (typeof actor == "string") {
|
||||
return `<a href="${escapeHTML(actor)}" target="_parent" rel="nofollow">${escapeHTML(actor)}</a>`;
|
||||
}
|
||||
|
||||
return `<a title="${escapeHTML(actor.name)}" href="${escapeHTML(actor.url)}" target="_parent" rel="nofollow"><img class="profile" src="${escapeHTML(actor.icon.url)}" alt="The profile picture of ${escapeHTML(actor.name)}"></a>`
|
||||
}
|
||||
).join("")}
|
||||
</section>
|
||||
<section class="replies">
|
||||
<h4>Replies (${repliesCount})</h4>
|
||||
${repliesSnapshot.docs.map(doc => {
|
||||
const { actor, object } = doc.data();
|
||||
if (typeof actor == "string") {
|
||||
return `<div><a href="${escapeHTML(actor)}" target="_parent" rel="nofollow">${escapeHTML(actor)}</a> wrote: <blockquote>${escapeHTML(stripHTML(object.content))}</blockquote></div>`;
|
||||
}
|
||||
|
||||
return `<div class="reply">
|
||||
<p><a title="${escapeHTML(actor.name)}" href="${escapeHTML(actor.url)}" target="_parent" rel="nofollow"><img class="profile" src="${escapeHTML(actor.icon.url)}" alt="The profile picture of ${escapeHTML(actor.name)}"></a>${escapeHTML(actor.name)} wrote: <blockquote>${escapeHTML(stripHTML(object.content))}</blockquote></p>
|
||||
|
||||
</div>`
|
||||
}
|
||||
).join("")}
|
||||
</section>
|
||||
</body>
|
||||
</html>`);
|
||||
}
|
39
api/activitypub/actor.ts
Normal file
39
api/activitypub/actor.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import type { VercelRequest, VercelResponse } from '@vercel/node';
|
||||
|
||||
export default function (req: VercelRequest, res: VercelResponse) {
|
||||
const { headers } = req;
|
||||
|
||||
if ("accept" in headers) {
|
||||
const accept = headers["accept"];
|
||||
if (accept != null && accept.split(",").indexOf("text/html") > -1) {
|
||||
return res.redirect(302, "https://flear.org/").end();
|
||||
}
|
||||
}
|
||||
|
||||
res.statusCode = 200;
|
||||
res.setHeader("Content-Type", `application/activity+json`);
|
||||
res.json({
|
||||
"@context": ["https://www.w3.org/ns/activitystreams", { "@language": "en- GB" }],
|
||||
"type": "Person",
|
||||
"id": "https://flear.org/flear",
|
||||
"outbox": "https://flear.org/outbox",
|
||||
"following": "https://flear.org/following",
|
||||
"followers": "https://flear.org/followers",
|
||||
"inbox": "https://flear.org/inbox",
|
||||
"preferredUsername": "flear",
|
||||
"name": "FLEAR (Free & Libre Engineers for Amateur Radio)",
|
||||
"summary": "FLEAR is non-profit promoting open-source and open-standards in Amateur Radio",
|
||||
"icon": {
|
||||
"type": "Image",
|
||||
"mediaType": "image/png",
|
||||
"url": "https://flear.org/images/logo.png"
|
||||
},
|
||||
"publicKey": {
|
||||
"@context": "https://w3id.org/security/v1",
|
||||
"@type": "Key",
|
||||
"id": "https://flear.org/flear#main-key",
|
||||
"owner": "https://flear.org/flear",
|
||||
"publicKeyPem": process.env.ACTIVITYPUB_PUBLIC_KEY
|
||||
}
|
||||
});
|
||||
}
|
7
api/activitypub/authorize_interaction.ts
Normal file
7
api/activitypub/authorize_interaction.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import type { VercelRequest, VercelResponse } from '@vercel/node';
|
||||
|
||||
export default function (req: VercelRequest, res: VercelResponse) {
|
||||
res.statusCode = 200;
|
||||
res.setHeader("Content-Type", `application/jrd+json`);
|
||||
res.end('ok');
|
||||
};
|
32
api/activitypub/followers.ts
Normal file
32
api/activitypub/followers.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import type { VercelRequest, VercelResponse } from '@vercel/node';
|
||||
import * as admin from 'firebase-admin';
|
||||
|
||||
|
||||
if (!admin.default.apps.length) {
|
||||
admin.default.initializeApp({
|
||||
credential: admin.default.credential.cert({
|
||||
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
|
||||
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
|
||||
privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n')
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
const db = admin.default.firestore();
|
||||
|
||||
export default async function (req: VercelRequest, res: VercelResponse) {
|
||||
const collection = db.collection('followers');
|
||||
const actors = await collection.select("actor").get();
|
||||
|
||||
const output = {
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"id": "https://qoto.org/users/flear/following?page=1",
|
||||
"type": "OrderedCollectionPage",
|
||||
"totalItems": actors.docs.length,
|
||||
"orderedItems": actors.docs.map(item=>item.get("actor"))
|
||||
}
|
||||
|
||||
res.statusCode = 200;
|
||||
res.setHeader("Content-Type", `application/activity+json`);
|
||||
res.json(output);
|
||||
};
|
7
api/activitypub/following.ts
Normal file
7
api/activitypub/following.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import type { VercelRequest, VercelResponse } from '@vercel/node';
|
||||
|
||||
export default function (req: VercelRequest, res: VercelResponse) {
|
||||
res.statusCode = 200;
|
||||
res.setHeader("Content-Type", `application/activity+json`);
|
||||
res.end('ok');
|
||||
};
|
340
api/activitypub/inbox.ts
Normal file
340
api/activitypub/inbox.ts
Normal file
|
@ -0,0 +1,340 @@
|
|||
import type { VercelRequest, VercelResponse } from '@vercel/node';
|
||||
import { AP } from 'activitypub-core-types';
|
||||
import { CoreObject, EntityReference } from 'activitypub-core-types/lib/activitypub/index.js';
|
||||
|
||||
import * as admin from 'firebase-admin';
|
||||
|
||||
import type { Readable } from 'node:stream';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { fetchActorInformation } from '../../lib/activitypub/utils/fetchActorInformation.js';
|
||||
import { parseSignature } from '../../lib/activitypub/utils/parseSignature.js';
|
||||
import { sendSignedRequest } from '../../lib/activitypub/utils/sendSignedRequest.js';
|
||||
import { verifySignature } from '../../lib/activitypub/utils/verifySignature.js';
|
||||
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
||||
|
||||
if (!admin.default.apps.length) {
|
||||
admin.default.initializeApp({
|
||||
credential: admin.default.credential.cert({
|
||||
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
|
||||
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
|
||||
privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n')
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
const db = admin.default.firestore();
|
||||
|
||||
async function buffer(readable: Readable) {
|
||||
const chunks = [];
|
||||
for await (const chunk of readable) {
|
||||
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
|
||||
}
|
||||
return Buffer.concat(chunks);
|
||||
}
|
||||
|
||||
export default async function (req: VercelRequest, res: VercelResponse) {
|
||||
const { body, query, method, url, headers } = req;
|
||||
|
||||
res.statusCode = 200;
|
||||
res.setHeader("Content-Type", `application/activity+json`);
|
||||
|
||||
// Verify the message some how.
|
||||
const buf = await buffer(req);
|
||||
const rawBody = buf.toString('utf8');
|
||||
|
||||
const message = <AP.Activity>JSON.parse(rawBody);
|
||||
|
||||
if (message.type == "Delete") {
|
||||
// Ignore deletes for now.
|
||||
return res.end("delete")
|
||||
}
|
||||
|
||||
console.log(message);
|
||||
|
||||
const signature = parseSignature(req);
|
||||
const actorInformation = await fetchActorInformation(signature.keyId);
|
||||
const signatureValid = verifySignature(signature, actorInformation.publicKey);
|
||||
|
||||
if (signatureValid == null || signatureValid == false) {
|
||||
console.log("invalid signature");
|
||||
res.end('invalid signature');
|
||||
return;
|
||||
}
|
||||
|
||||
if (actorInformation != null) {
|
||||
await saveActor(actorInformation);
|
||||
// Add the actor information to the message so that it's saved directly.
|
||||
message.actor = actorInformation;
|
||||
}
|
||||
|
||||
console.log(message.type);
|
||||
|
||||
// We should check the digest.
|
||||
if (message.type == "Follow" && actorInformation != null) {
|
||||
// We are following.
|
||||
const acceptRequest = await saveFollow(<AP.Follow>message, actorInformation);
|
||||
res.end("ok");
|
||||
|
||||
const actorInbox = new URL(<URL>actorInformation.inbox);
|
||||
const response = await sendSignedRequest(actorInbox, acceptRequest);
|
||||
console.log("Following result", response.status, response.statusText, await response.text());
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type == "Like") {
|
||||
await saveLike(<AP.Like>message);
|
||||
res.end("ok");
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type == "Announce") {
|
||||
await saveAnnounce(<AP.Announce>message);
|
||||
|
||||
res.end("ok");
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type == "Create") {
|
||||
console.log("Message type Create")
|
||||
// Someone is sending us a message.
|
||||
const createMessage = <AP.Create>message;
|
||||
|
||||
if (createMessage == null || createMessage.id == null) return;
|
||||
if (createMessage.object == null) return;
|
||||
// We only interested in Replies - that is a "note" with a "replyTo"
|
||||
const createObject = <CoreObject>createMessage.object
|
||||
if (createObject.type == "Note" && createObject.inReplyTo != undefined) {
|
||||
await saveReply(<AP.Create>createMessage);
|
||||
}
|
||||
|
||||
res.end("ok");
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type == "Undo") {
|
||||
// Undo a follow.
|
||||
const undoObject: AP.Undo = <AP.Undo>message;
|
||||
if (undoObject == null || undoObject.id == null) return;
|
||||
if (undoObject.object == null) return;
|
||||
if ("actor" in undoObject.object == false) return;
|
||||
|
||||
if ((<CoreObject>undoObject.object).type == "Follow") {
|
||||
await removeFollow(<AP.Follow>undoObject);
|
||||
}
|
||||
|
||||
if ((<CoreObject>undoObject.object).type == "Like") {
|
||||
await removeLike(<AP.Like>undoObject);
|
||||
}
|
||||
|
||||
if ((<CoreObject>undoObject.object).type == "Announce") {
|
||||
await removeAnnounce(<AP.Announce>undoObject);
|
||||
}
|
||||
|
||||
res.end("ok");
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type == "Update") {
|
||||
// TODO: We need to update the messages
|
||||
console.log("Update message", message);
|
||||
res.end("ok");
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(401).end("Unknown message type");
|
||||
};
|
||||
|
||||
const getActorId = (actor: AP.EntityReference): string => {
|
||||
if (typeof actor == "string") {
|
||||
return actor;
|
||||
}
|
||||
else if (actor instanceof URL) {
|
||||
return actor.toString()
|
||||
}
|
||||
else {
|
||||
return (actor.id || "").toString();
|
||||
}
|
||||
}
|
||||
|
||||
async function removeFollow(message: AP.Follow) {
|
||||
// If from Mastodon - someone unfollowed me, we need to delete it from the store.
|
||||
const docId = getActorId(<EntityReference>message.actor).replace(/\//g, "_");
|
||||
|
||||
console.log("DocId to delete", docId);
|
||||
|
||||
const res = await db.collection('followers').doc(docId).delete();
|
||||
|
||||
console.log("Deleted", res);
|
||||
}
|
||||
|
||||
async function removeLike(message: AP.Like) {
|
||||
// If from Mastodon - someone un-liked the post. We need to delete it from the store.
|
||||
const doc = message.object?.object.toString().replace(/\//g, "_");
|
||||
const actorId = message.object?.id.toString().replace(/\//g, "_");
|
||||
|
||||
console.log(`Attempting to delete Like ${actorId} on ${doc}`);
|
||||
|
||||
const res = await db.collection('likes').doc(doc).collection('messages').doc(actorId).delete();
|
||||
|
||||
console.log(`Deleted Like ${actorId} on ${doc}`, res);
|
||||
}
|
||||
|
||||
async function removeAnnounce(message: AP.Announce) {
|
||||
// If from Mastodon - someone un-liked the post. We need to delete it from the store.
|
||||
const doc = message.object?.object.toString().replace(/\//g, "_");
|
||||
const actorId = message.object?.id.toString().replace(/\//g, "_");
|
||||
|
||||
console.log(`Attempting to delete Announce ${actorId} on ${doc}`);
|
||||
|
||||
const res = await db.collection('announces').doc(doc).collection('messages').doc(actorId).delete();
|
||||
|
||||
console.log(`Deleted Announce ${actorId} on ${doc}`, res);
|
||||
}
|
||||
|
||||
// Save the Actor objects so that we have a cache of them.
|
||||
// Note: We will be embeddeding the actor information in the messages for saving directly so we can do less reads.
|
||||
async function saveActor(message: AP.Actor) {
|
||||
if (message.id == null) return;
|
||||
const collection = db.collection('actors');
|
||||
const actorId = message.id.toString().replace(/\//g, "_");
|
||||
|
||||
const actorDocRef = collection.doc(actorId);
|
||||
|
||||
// Create the follow;
|
||||
await actorDocRef.set(message); // Always update the actor.
|
||||
}
|
||||
|
||||
async function saveFollow(message: AP.Follow, actorInformation: AP.Actor) {
|
||||
if (message.id == null) return;
|
||||
|
||||
const collection = db.collection('followers');
|
||||
|
||||
const actorID = getActorId(<EntityReference>message.actor).toString();
|
||||
const followDocRef = collection.doc(actorID.replace(/\//g, "_"));
|
||||
const followDoc = await followDocRef.get();
|
||||
|
||||
if (followDoc.exists) {
|
||||
console.log("Already Following");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the follow;
|
||||
await followDocRef.set(message);
|
||||
|
||||
const guid = uuid();
|
||||
const domain = 'flear.org';
|
||||
|
||||
const acceptRequest: AP.Accept = <AP.Accept>{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
'id': `https://${domain}/${guid}`,
|
||||
'type': 'Accept',
|
||||
'actor': "https://flear.org/flear",
|
||||
'object': message.id
|
||||
};
|
||||
|
||||
return acceptRequest;
|
||||
}
|
||||
|
||||
async function saveLike(message: AP.Like) {
|
||||
// If from Mastodon - someone liked the post.
|
||||
const collection = db.collection('likes');
|
||||
|
||||
// We should do some checks
|
||||
// 1. TODO: in reply to is against a post that I made.
|
||||
|
||||
console.log("Save Like", message);
|
||||
|
||||
/*
|
||||
We store likes as a collection of collections.
|
||||
Root key is the url of my messages
|
||||
Each object has a sub-collection of the specific message made by someone.
|
||||
*/
|
||||
const id = (<URL>message.id).toString();
|
||||
const objectId = (<URL>message.object).toString();
|
||||
const rootDocRef = collection.doc(objectId.replace(/\//g, "_"));
|
||||
const rootDoc = await rootDocRef.get();
|
||||
|
||||
if (rootDoc.exists == false) {
|
||||
console.log("Root doesn't exists, make it so.");
|
||||
await rootDocRef.set({});
|
||||
}
|
||||
|
||||
const messagesCollection = rootDocRef.collection('messages');
|
||||
const messageDocRef = messagesCollection.doc(id.replace(/\//g, "_"));
|
||||
const messageDoc = await messageDocRef.get();
|
||||
|
||||
if (messageDoc.exists == false) {
|
||||
console.log(`Adding message "${id}" to ${objectId}`);
|
||||
await messageDocRef.set(message);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveAnnounce(message: AP.Announce) {
|
||||
// If from Mastodon - someone boosted the post.
|
||||
const collection = db.collection('announces');
|
||||
|
||||
// We should do some checks
|
||||
// 1. TODO: in reply to is against a post that I made.
|
||||
|
||||
console.log("Save Announce", message);
|
||||
|
||||
/*
|
||||
We store announces as a collection of collections.
|
||||
Root key is the url of my messages
|
||||
Each object has a sub-collection of the specific message made by someone.
|
||||
*/
|
||||
const id = (<URL>message.id).toString();
|
||||
const objectId = (<URL>message.object).toString();
|
||||
const rootDocRef = collection.doc(objectId.replace(/\//g, "_"));
|
||||
const rootDoc = await rootDocRef.get();
|
||||
|
||||
if (rootDoc.exists == false) {
|
||||
await rootDocRef.set({});
|
||||
}
|
||||
|
||||
const messagesCollection = rootDocRef.collection('messages');
|
||||
const messageDocRef = messagesCollection.doc(id.replace(/\//g, "_"));
|
||||
const messageDoc = await messageDocRef.get();
|
||||
|
||||
if (messageDoc.exists == false) {
|
||||
console.log(`Adding message "${id}" to ${objectId}`);
|
||||
await messageDocRef.set(message);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveReply(message: AP.Create) {
|
||||
// If from Mastodon - someone boosted the post.
|
||||
const collection = db.collection('replies');
|
||||
|
||||
if (message.object == undefined) return;
|
||||
|
||||
// We should do some checks
|
||||
// 1. TODO: in reply to is against a post that I made.
|
||||
|
||||
console.log("Save Reply", message);
|
||||
|
||||
/*
|
||||
We store announces as a collection of collections.
|
||||
Root key is the url of my messages
|
||||
Each object has a sub-collection of the specific message made by someone.
|
||||
*/
|
||||
const objectId = (<URL>(<CoreObject>message.object).inReplyTo).toString();
|
||||
const rootDocRef = collection.doc(objectId.replace(/\//g, "_"));
|
||||
const rootDoc = await rootDocRef.get();
|
||||
|
||||
if (rootDoc.exists == false) {
|
||||
await rootDocRef.set({});
|
||||
}
|
||||
|
||||
const messagesCollection = rootDocRef.collection('messages');
|
||||
|
||||
const id = (<URL>message.id).toString();
|
||||
const messageDocRef = messagesCollection.doc(id.replace(/\//g, "_"));
|
||||
const messageDoc = await messageDocRef.get();
|
||||
|
||||
if (messageDoc.exists == false) {
|
||||
console.log(`Adding message "${id}" to ${objectId}`);
|
||||
await messageDocRef.set(message);
|
||||
}
|
||||
}
|
20
api/activitypub/outbox.ts
Normal file
20
api/activitypub/outbox.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import type { VercelRequest, VercelResponse } from '@vercel/node';
|
||||
import { join } from 'path';
|
||||
import { cwd } from 'process';
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
/*
|
||||
This returns a list of posts for the single user 'FLEAR'.
|
||||
|
||||
It's a GET request. This doesn't post it to anyone's timeline.
|
||||
*/
|
||||
export default function (req: VercelRequest, res: VercelResponse) {
|
||||
// All of the outbox data is generated at build time, so just return that static file.
|
||||
const file = join(cwd(), 'public', 'outbox.ajson');
|
||||
const stringified = readFileSync(file, 'utf8');
|
||||
|
||||
res.statusCode = 200;
|
||||
res.setHeader("Content-Type", `application/activity+json`);
|
||||
|
||||
return res.end(stringified);
|
||||
};
|
123
api/activitypub/sendNote.ts
Normal file
123
api/activitypub/sendNote.ts
Normal file
|
@ -0,0 +1,123 @@
|
|||
import type { VercelRequest, VercelResponse } from '@vercel/node';
|
||||
import { AP } from 'activitypub-core-types';
|
||||
import * as admin from 'firebase-admin';
|
||||
import { OrderedCollection } from 'activitypub-core-types/lib/activitypub/index.js';
|
||||
import { sendSignedRequest } from '../../lib/activitypub/utils/sendSignedRequest.js';
|
||||
import { fetchActorInformation } from '../../lib/activitypub/utils/fetchActorInformation.js';
|
||||
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
||||
|
||||
if (!admin.default.apps.length) {
|
||||
admin.default.initializeApp({
|
||||
credential: admin.default.credential.cert({
|
||||
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
|
||||
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
|
||||
privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n')
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
const db = admin.default.firestore();
|
||||
|
||||
/*
|
||||
Sends the latest not that hasn't yet been sent.
|
||||
*/
|
||||
export default async function (req: VercelRequest, res: VercelResponse) {
|
||||
const { body, query, method, url, headers } = req;
|
||||
const { token } = query;
|
||||
|
||||
if (method != "GET") {
|
||||
console.log("Invalid Method, must be GET");
|
||||
res.status(401).end("Invalid Method, must be GET");
|
||||
return;
|
||||
}
|
||||
|
||||
const configCollection = db.collection('config');
|
||||
const configRef = configCollection.doc("config");
|
||||
const config = await configRef.get();
|
||||
|
||||
if (config.exists == false) {
|
||||
// Config doesn't exist, make something
|
||||
configRef.set({
|
||||
"lastId": "",
|
||||
"lastEpoch": ""
|
||||
});
|
||||
}
|
||||
|
||||
const configData = config.data();
|
||||
let lastId = "";
|
||||
if (configData != undefined) {
|
||||
lastId = configData.lastId;
|
||||
let lastEpoch = configData.lastEpoch;
|
||||
let currentEpoch = new Date().getTime();
|
||||
let elapsed = currentEpoch - lastEpoch;
|
||||
|
||||
if (elapsed < process.env.POLL_MILLISECONDS) {
|
||||
console.log("Function called too often, doing nothing");
|
||||
res.status(401).end("Function is rate limited, please wait")
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Get my outbox because it contains all my notes.
|
||||
const outboxResponse = await fetch('https://flear.org/outbox');
|
||||
const outbox = <OrderedCollection>(await outboxResponse.json());
|
||||
|
||||
const followersCollection = db.collection('followers');
|
||||
const followersQuerySnapshot = await followersCollection.get();
|
||||
|
||||
let lastSuccessfulSentId = "";
|
||||
|
||||
for (const followerDoc of followersQuerySnapshot.docs) {
|
||||
const follower = followerDoc.data();
|
||||
try {
|
||||
const actorUrl = (typeof follower.actor == "string") ? follower.actor : follower.actor.id;
|
||||
const actorInformation = await fetchActorInformation(actorUrl);
|
||||
if (actorInformation == undefined) {
|
||||
// We can't send to this actor, so skip it. We should log it.
|
||||
continue;
|
||||
}
|
||||
|
||||
const actorInbox = new URL(<URL>actorInformation.inbox);
|
||||
|
||||
for (const iteIdx in (<AP.EntityReference[]>outbox.orderedItems)) {
|
||||
// We have to break somewhere... do it after the first.
|
||||
const item = (<AP.EntityReference[]>outbox.orderedItems)[iteIdx];
|
||||
console.log(`Checking ID ${item.id}, ${lastId}`);
|
||||
if (item.id == `${lastId}`) {
|
||||
lastSuccessfulSentId = item.id;
|
||||
// We've already posted this, don't try and send it again.
|
||||
console.log(`${item.id} has already been posted - don't attempt`)
|
||||
break;
|
||||
}
|
||||
|
||||
if (item.object != undefined) {
|
||||
// We might not need this.
|
||||
item.object.published = (new Date()).toISOString();
|
||||
}
|
||||
|
||||
console.log(`Sending to ${actorInbox}`);
|
||||
|
||||
// Item will be an entity, i.e, { Create { Note } }
|
||||
const response = await sendSignedRequest(actorInbox, <AP.Activity> item);
|
||||
console.log(`Send result: ${actorInbox}`, response.status, response.statusText, await response.text());
|
||||
|
||||
// It's not been sent.
|
||||
lastSuccessfulSentId = item.id; // we shouldn't really set this everytime.
|
||||
|
||||
//break; // At some point we might want to post more than one post, so remove this.
|
||||
}
|
||||
} catch (ex) {
|
||||
console.log("Error", ex);
|
||||
}
|
||||
}
|
||||
|
||||
configRef.set({
|
||||
"lastId": lastSuccessfulSentId,
|
||||
"lastEpoch": new Date().getTime()
|
||||
});
|
||||
|
||||
console.log("sendNode successful")
|
||||
|
||||
res.status(200).end("ok");
|
||||
};
|
20
api/card.ts
Normal file
20
api/card.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { ImageResponse } from '@vercel/og';
|
||||
|
||||
export const config = {
|
||||
runtime: 'edge'
|
||||
}
|
||||
|
||||
export default function (req: Request) {
|
||||
|
||||
const url = new URL(req.url);
|
||||
const title = url.searchParams.get("title");
|
||||
const imgUrl = url.searchParams.get("imgUrl") || "https://flear.org/images/logo.png";
|
||||
const width = url.searchParams.get("width") || "800"
|
||||
const height = url.searchParams.get("height") || "418";
|
||||
|
||||
return new ImageResponse({ "type": "div", "props": { "style": { "display": "flex", "height": "100%", "width": "100%", "alignItems": "center", "justifyContent": "center", "letterSpacing": "-.02em", "fontWeight": 700, "background": "white" }, "children": [{ "type": "div", "props": { "style": { "left": 42, "top": 42, "position": "absolute", "display": "flex", "alignItems": "center" }, "children": [{ "type": "span", "props": { "style": { "width": 24, "height": 24, "background": "black" } } }, { "type": "span", "props": { "style": { "marginLeft": 8, "fontSize": 20 }, "children": "flear.org" } }] } }, { "type": "div", "props": { "style": { "display": "flex", "flexWrap": "wrap", "justifyContent": "center", "padding": "20px 50px", "margin": "0 42px", "fontSize": 40, "width": "auto", "maxWidth": 550, "textAlign": "center", "backgroundColor": "black", "color": "white", "lineHeight": 1.4 }, "children": title } }] } },
|
||||
{
|
||||
width,
|
||||
height
|
||||
}); // 800px by 418px
|
||||
}
|
106
api/mentions.js
Normal file
106
api/mentions.js
Normal file
|
@ -0,0 +1,106 @@
|
|||
const html = require('whatwg-flora-tmpl');
|
||||
const { Readable } = require('stream');
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
const sanitize = (str) => {
|
||||
if (str == null || str == undefined) return "";
|
||||
return str.replace('<', '<').replace('>', '>').replace('"', '"');
|
||||
};
|
||||
|
||||
const render = (data) => html`<html><head><title>Interactions with ${data.url}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
|
||||
body {
|
||||
font-family: Helvetica,Arial,sans-serif;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.webmentions .comments .reply img {
|
||||
float: left;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
img.profile.photo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50% 50%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
color: #fefefe;
|
||||
background-color: #212529;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #1bcba2
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #7ad857
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="comments webmentions">
|
||||
<h4>Likes and bookmarks</h4>
|
||||
${data.filter(item => item['wm-property'] === 'like-of' || item['wm-property'] === 'bookmark-of').map(item => html`<a href="${sanitize(item.author.url)}" target="_blank"><img src="${sanitize(item.author.photo)}" alt="${sanitize(item.author.name)}" class="profile photo" loading="lazy"></a>`)}
|
||||
<h4>Reposts</h4>
|
||||
${data.filter(item => item['wm-property'] === 'repost-of').map(item => html`<a href="${sanitize(item.author.url)}" target="_blank"><img src="${sanitize(item.author.photo)}" alt="${sanitize(item.author.name)}" class="profile photo" loading="lazy"></a>`)}
|
||||
<h4>Comments and Replies</h4>
|
||||
<div class="comments">
|
||||
${data.filter(item => item['wm-property'] === 'in-reply-to' || item['wm-property'] === 'mention-of').map(item => html`<div class="reply">
|
||||
<a href="${item.url}" target="_blank"><img src="${sanitize(item.author.photo)}" alt="${sanitize(item.author.name)}" class="profile photo" loading="lazy"></a><span><a href="${sanitize(item.url)}" target="_blank">${sanitize(item.author.name)}</a></span>
|
||||
<blockquote>${sanitize(item.content?.text)}</blockquote>
|
||||
</div>`)}
|
||||
</div>
|
||||
</div>
|
||||
</body></html>`;
|
||||
|
||||
class FromWhatWGReadableStream extends Readable {
|
||||
constructor(options, whatwgStream) {
|
||||
super(options);
|
||||
const streamReader = whatwgStream.getReader();
|
||||
const outStream = this;
|
||||
|
||||
function pump() {
|
||||
return streamReader.read().then(({ value, done }) => {
|
||||
if (done) {
|
||||
outStream.push(null);
|
||||
return;
|
||||
}
|
||||
|
||||
outStream.push(value);
|
||||
return pump();
|
||||
});
|
||||
}
|
||||
|
||||
pump();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = async (req, res) => {
|
||||
const { url = 'https://flear.org/', count = 200 } = req.query;
|
||||
const referer = req.headers.referer;
|
||||
const cacheAge = 12 * 60 * 60;
|
||||
|
||||
const mentionsUrl = `https://webmention.io/api/mentions.jf2?per-page=${count}&target=${referer || url}`;
|
||||
|
||||
try {
|
||||
const mentionsResponse = await fetch(mentionsUrl);
|
||||
const data = await mentionsResponse.json();
|
||||
data.url = referer || url;
|
||||
// Add caching.
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', `text/html; charset=utf-8`);
|
||||
res.setHeader('Cache-Control', `public,s-maxage=${cacheAge}, max-age=${cacheAge}`);
|
||||
const output = await render(data.children);
|
||||
const stream = new FromWhatWGReadableStream({}, output);
|
||||
stream.pipe(res, { end: true });
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
res.status(500).send(ex);
|
||||
}
|
||||
}
|
33
api/nodeinfo/2.1.ts
Normal file
33
api/nodeinfo/2.1.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import type { VercelRequest, VercelResponse } from '@vercel/node';
|
||||
|
||||
export default function (req: VercelRequest, res: VercelResponse) {
|
||||
res.statusCode = 200;
|
||||
res.setHeader("Content-Type", `application/json`);
|
||||
res.json({
|
||||
"version": "2.1",
|
||||
"software": {
|
||||
"name": "flear.org",
|
||||
"repository": "https://git.qoto.org/flear/flear-site",
|
||||
"homepage": "https://flear.org/",
|
||||
"version": "0.0.1"
|
||||
},
|
||||
"protocols": [
|
||||
"activitypub"
|
||||
],
|
||||
"services": {
|
||||
"inbound": ["atom1.0", "rss2.0"],
|
||||
"outbound": ["atom1.0", "rss2.0"]
|
||||
},
|
||||
"openRegistrations": false,
|
||||
"usage": {
|
||||
"users": {
|
||||
"total": 1,
|
||||
"activeHalfyear": 1,
|
||||
"activeMonth": 1
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"nodeName": "flear.org"
|
||||
}
|
||||
});
|
||||
}
|
15
api/well-known/nodeinfo.ts
Normal file
15
api/well-known/nodeinfo.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import type { VercelRequest, VercelResponse } from '@vercel/node';
|
||||
|
||||
export default function (req: VercelRequest, res: VercelResponse) {
|
||||
const { resource } = req.query;
|
||||
res.statusCode = 200;
|
||||
res.setHeader("Content-Type", `application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.1#"`);
|
||||
res.json({
|
||||
"links": [
|
||||
{
|
||||
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.1",
|
||||
"href": "https://flear.org/nodeinfo/2.1"
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
20
api/well-known/webfinger.ts
Normal file
20
api/well-known/webfinger.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import type { VercelRequest, VercelResponse } from '@vercel/node';
|
||||
|
||||
export default function (req: VercelRequest, res: VercelResponse) {
|
||||
const { resource } = req.query;
|
||||
res.statusCode = 200;
|
||||
res.setHeader("Content-Type", `application/jrd+json`);
|
||||
res.end(`{
|
||||
"subject": "acct:flear@flear.org",
|
||||
"aliases": [
|
||||
"https://qoto.org/@flear"
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"type": "application/activity+json",
|
||||
"href": "https://flear.org/flear"
|
||||
}
|
||||
]
|
||||
}`);
|
||||
}
|
5
archetypes/default.md
Normal file
5
archetypes/default.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: "{{ replace .Name "-" " " | title }}"
|
||||
date: {{ .Date }}
|
||||
draft: true
|
||||
---
|
7970
assets/css/fontawesome.css
vendored
Normal file
7970
assets/css/fontawesome.css
vendored
Normal file
File diff suppressed because it is too large
Load diff
4833
assets/css/ghost.css
Normal file
4833
assets/css/ghost.css
Normal file
File diff suppressed because it is too large
Load diff
800
assets/css/glyphicons.css
Normal file
800
assets/css/glyphicons.css
Normal file
|
@ -0,0 +1,800 @@
|
|||
@font-face {
|
||||
font-family: 'Glyphicons Halflings';
|
||||
src: url(../fonts/glyphicons-halflings-regular.eot);
|
||||
src: url(../fonts/glyphicons-halflings-regular.eot?#iefix) format("embedded-opentype"), url(../fonts/glyphicons-halflings-regular.woff2) format("woff2"), url(../fonts/glyphicons-halflings-regular.woff) format("woff"), url(../fonts/glyphicons-halflings-regular.ttf) format("truetype"), url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format("svg"); }
|
||||
.glyphicon {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
display: inline-block;
|
||||
font-family: 'Glyphicons Halflings';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale; }
|
||||
|
||||
.glyphicon-asterisk:before {
|
||||
content: "*"; }
|
||||
|
||||
.glyphicon-plus:before {
|
||||
content: "+"; }
|
||||
|
||||
.glyphicon-eur:before, .glyphicon-euro:before {
|
||||
content: "€"; }
|
||||
|
||||
.glyphicon-minus:before {
|
||||
content: "−"; }
|
||||
|
||||
.glyphicon-cloud:before {
|
||||
content: "☁"; }
|
||||
|
||||
.glyphicon-envelope:before {
|
||||
content: "✉"; }
|
||||
|
||||
.glyphicon-pencil:before {
|
||||
content: "✏"; }
|
||||
|
||||
.glyphicon-glass:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-music:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-search:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-heart:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-star:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-star-empty:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-user:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-film:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-th-large:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-th:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-th-list:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-ok:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-remove:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-zoom-in:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-zoom-out:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-off:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-signal:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-cog:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-trash:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-home:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-file:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-time:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-road:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-download-alt:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-download:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-upload:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-inbox:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-play-circle:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-repeat:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-refresh:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-list-alt:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-lock:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-flag:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-headphones:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-volume-off:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-volume-down:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-volume-up:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-qrcode:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-barcode:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-tag:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-tags:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-book:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-bookmark:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-print:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-camera:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-font:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-bold:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-italic:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-text-height:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-text-width:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-align-left:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-align-center:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-align-right:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-align-justify:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-list:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-indent-left:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-indent-right:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-facetime-video:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-picture:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-map-marker:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-adjust:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-tint:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-edit:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-share:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-check:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-move:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-step-backward:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-fast-backward:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-backward:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-play:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-pause:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-stop:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-forward:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-fast-forward:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-step-forward:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-eject:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-chevron-left:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-chevron-right:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-plus-sign:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-minus-sign:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-remove-sign:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-ok-sign:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-question-sign:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-info-sign:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-screenshot:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-remove-circle:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-ok-circle:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-ban-circle:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-arrow-left:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-arrow-right:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-arrow-up:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-arrow-down:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-share-alt:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-resize-full:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-resize-small:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-exclamation-sign:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-gift:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-leaf:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-fire:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-eye-open:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-eye-close:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-warning-sign:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-plane:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-calendar:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-random:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-comment:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-magnet:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-chevron-up:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-chevron-down:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-retweet:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-shopping-cart:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-folder-close:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-folder-open:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-resize-vertical:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-resize-horizontal:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-hdd:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-bullhorn:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-bell:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-certificate:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-thumbs-up:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-thumbs-down:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-hand-right:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-hand-left:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-hand-up:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-hand-down:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-circle-arrow-right:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-circle-arrow-left:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-circle-arrow-up:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-circle-arrow-down:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-globe:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-wrench:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-tasks:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-filter:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-briefcase:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-fullscreen:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-dashboard:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-paperclip:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-heart-empty:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-link:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-phone:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-pushpin:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-usd:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-gbp:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-sort:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-sort-by-alphabet:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-sort-by-alphabet-alt:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-sort-by-order:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-sort-by-order-alt:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-sort-by-attributes:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-sort-by-attributes-alt:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-unchecked:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-expand:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-collapse-down:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-collapse-up:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-log-in:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-flash:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-log-out:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-new-window:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-record:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-save:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-open:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-saved:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-import:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-export:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-send:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-floppy-disk:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-floppy-saved:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-floppy-remove:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-floppy-save:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-floppy-open:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-credit-card:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-transfer:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-cutlery:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-header:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-compressed:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-earphone:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-phone-alt:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-tower:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-stats:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-sd-video:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-hd-video:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-subtitles:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-sound-stereo:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-sound-dolby:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-sound-5-1:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-sound-6-1:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-sound-7-1:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-copyright-mark:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-registration-mark:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-cloud-download:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-cloud-upload:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-tree-conifer:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-tree-deciduous:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-cd:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-save-file:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-open-file:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-level-up:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-copy:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-paste:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-alert:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-equalizer:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-king:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-queen:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-pawn:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-bishop:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-knight:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-baby-formula:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-tent:before {
|
||||
content: "⛺"; }
|
||||
|
||||
.glyphicon-blackboard:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-bed:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-apple:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-erase:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-hourglass:before {
|
||||
content: "⌛"; }
|
||||
|
||||
.glyphicon-lamp:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-duplicate:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-piggy-bank:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-scissors:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-bitcoin:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-btc:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-xbt:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-yen:before {
|
||||
content: "¥"; }
|
||||
|
||||
.glyphicon-jpy:before {
|
||||
content: "¥"; }
|
||||
|
||||
.glyphicon-ruble:before {
|
||||
content: "₽"; }
|
||||
|
||||
.glyphicon-rub:before {
|
||||
content: "₽"; }
|
||||
|
||||
.glyphicon-scale:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-ice-lolly:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-ice-lolly-tasted:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-education:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-option-horizontal:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-option-vertical:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-menu-hamburger:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-modal-window:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-oil:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-grain:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-sunglasses:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-text-size:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-text-color:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-text-background:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-object-align-top:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-object-align-bottom:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-object-align-horizontal:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-object-align-left:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-object-align-vertical:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-object-align-right:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-triangle-right:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-triangle-left:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-triangle-bottom:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-triangle-top:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-console:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-superscript:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-subscript:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-menu-left:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-menu-right:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-menu-down:before {
|
||||
content: ""; }
|
||||
|
||||
.glyphicon-menu-up:before {
|
||||
content: ""; }
|
3728
assets/css/goblin.css
Normal file
3728
assets/css/goblin.css
Normal file
File diff suppressed because it is too large
Load diff
631
assets/css/main.css
Normal file
631
assets/css/main.css
Normal file
|
@ -0,0 +1,631 @@
|
|||
div.widgets {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#mentions, #fediverse {
|
||||
border: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
img.profile.photo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50% 50%;
|
||||
}
|
||||
|
||||
aside {
|
||||
background-color: wheat;
|
||||
color: black;
|
||||
padding: 1em;
|
||||
border-radius: 0.5em;
|
||||
margin: 1em;
|
||||
border: dashed 3px #b78529;
|
||||
}
|
||||
|
||||
.me {
|
||||
margin: auto;
|
||||
text-align: left;
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
.me div {
|
||||
margin: 2em;
|
||||
}
|
||||
|
||||
.me img {
|
||||
border-radius: 50%;
|
||||
float: right;
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
pre {
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body.list .post header {
|
||||
padding-top: 1em;
|
||||
margin-bottom: 2em;
|
||||
font-family: var(--font-sans)
|
||||
}
|
||||
|
||||
body.blogpost header {
|
||||
max-width: 720px;
|
||||
margin: auto;
|
||||
text-align: left;
|
||||
padding: 0 1em;
|
||||
font-family: var(--font-sans)
|
||||
}
|
||||
|
||||
:where(:not(pre)>code) {
|
||||
white-space: break-spaces;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
body.blogpost hr {
|
||||
height: 1px;
|
||||
margin-right: 2em;
|
||||
margin-left: 2em;
|
||||
border: 0;
|
||||
background: #333;
|
||||
background-image: linear-gradient(to right, #ccc, #333, #ccc);
|
||||
}
|
||||
|
||||
body.blogpost div.draft {
|
||||
border: solid red 1px;
|
||||
background-color: pink;
|
||||
color: black;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
body.blogpost div.draft a {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
body.blogpost header,
|
||||
body.list main .post header {
|
||||
background-repeat: repeat;
|
||||
background-position: 50% 50%;
|
||||
background-size: cover;
|
||||
background-origin: padding-box;
|
||||
background-attachment: scroll;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body.blogpost header.with-background,
|
||||
body.list main .post header.with-background {
|
||||
height: 520px;
|
||||
margin: 0;
|
||||
margin-bottom: 2em;
|
||||
width: 100%;
|
||||
max-width: initial;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
body.blogpost .byline {
|
||||
margin-bottom: 1em;
|
||||
padding-bottom: 1em;
|
||||
border-bottom: solid 1px #e9e9e9;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
div.subscribe,
|
||||
div.share {
|
||||
text-decoration: none;
|
||||
color: #2a2a2a;
|
||||
background-color: #2196f3;
|
||||
text-align: center;
|
||||
letter-spacing: 0.5px;
|
||||
transition: 0.2s ease-out;
|
||||
cursor: pointer;
|
||||
outline: 0;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
display: inline-block;
|
||||
height: 36px;
|
||||
padding: 0 1rem;
|
||||
text-transform: uppercase;
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12),
|
||||
0 3px 1px -2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.post.moi {
|
||||
background-color: var(--blue-10);
|
||||
color: var(--gray-3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 1rem;
|
||||
border-radius: 1em;
|
||||
}
|
||||
|
||||
section.post.moi {
|
||||
margin: 0;
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
.post.moi div>img {
|
||||
width: 3em;
|
||||
height: 3em;
|
||||
padding: 2em;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.post.moi div p {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.post.moi div+div {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.moi-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.post.moi div p+p {
|
||||
font-weight: inherit;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.post.moi a.email img,
|
||||
.post.moi a.rss img,
|
||||
.post.moi a.twitter img,
|
||||
.post.moi a.medium img {
|
||||
padding: 0.5em;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
body.blogpost figure img, main p img {
|
||||
width: 100%;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
|
||||
}
|
||||
|
||||
body.list main.main-wrapper,
|
||||
body.blogpost main.main-wrapper {
|
||||
margin: auto;
|
||||
display: flex;
|
||||
padding: 5em 1em 5em 1em;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
body.list main p,
|
||||
body.blogpost main p {
|
||||
margin-block-start: 1em;
|
||||
}
|
||||
|
||||
body main section {
|
||||
padding-top: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.author {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
body.list .post .description {
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
body .menu {
|
||||
font-family: var(--font-sans);
|
||||
font-weight: 400;
|
||||
color: #333;
|
||||
color: var(--text-color);
|
||||
padding: 1em;
|
||||
text-align: center;
|
||||
border-bottom: solid 1px #ccc;
|
||||
border-bottom: solid 1px var(--block-quote-before-color);
|
||||
}
|
||||
|
||||
body .menu .search {
|
||||
max-width: 720px;
|
||||
margin: auto;
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
||||
body .menu .search form {
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
body .menu .search input[type=search] {
|
||||
width: 100%;
|
||||
padding: 0.5em;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
body .menu .menu-items {
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
max-width: 720px;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
body .menu a {
|
||||
color: initial;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
figure {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
figure img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
figure video {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body.list figure {
|
||||
text-align: center;
|
||||
margin: 1em -1em;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body.list figure img {
|
||||
box-shadow: 0px 10px 10px -10px black;
|
||||
}
|
||||
|
||||
body.list figure video {
|
||||
box-shadow: 0px 10px 10px -10px black;
|
||||
}
|
||||
|
||||
figcaption {
|
||||
font-size: 0.7em;
|
||||
font-weight: 600;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
font-style: italic;
|
||||
position: relative;
|
||||
quotes: "\201C""\201D";
|
||||
}
|
||||
|
||||
div.podcast,
|
||||
div.video {
|
||||
margin: 1em 0em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div.podcast audio,
|
||||
div.video video {
|
||||
display: block;
|
||||
margin: 1em auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.widgets {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.webmentions .comments .reply img {
|
||||
float: left;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
div.subscribe,
|
||||
div.share {
|
||||
margin: 1em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
div.subscribe a {
|
||||
color: white;
|
||||
filter: contrast(100);
|
||||
}
|
||||
|
||||
div.subscribe a img {
|
||||
display: block;
|
||||
}
|
||||
|
||||
div.share {
|
||||
background-color: #ff4081;
|
||||
color: white;
|
||||
text-align: center;
|
||||
max-height: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.share svg {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
div.widgets.source a {
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
div.related.pages:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
/**************************************************************************
|
||||
** Some formatting tweaks after merging everything intentional...
|
||||
**************************************************************************/
|
||||
|
||||
|
||||
code {
|
||||
background-color: #f5f5f5;
|
||||
color: #000; }
|
||||
|
||||
.modal img {
|
||||
max-width: 100%; }
|
||||
|
||||
.navbar {
|
||||
margin-bottom: 0; }
|
||||
|
||||
.navbar-brand {
|
||||
padding: 0; }
|
||||
.navbar-brand img {
|
||||
height: 100%;
|
||||
padding: 15px;
|
||||
width: auto; }
|
||||
|
||||
.navbar-nav {
|
||||
border-left: 1px #e7e7e7 solid; }
|
||||
.navbar-nav li > a {
|
||||
font-size: 13px;
|
||||
font-weight: bold; }
|
||||
.navbar-nav .active > a {
|
||||
background-color: #f8f8f8; }
|
||||
.navbar-nav .active > a:hover {
|
||||
background-color: #f8f8f8; }
|
||||
.navbar-nav .subscribe-button {
|
||||
border-right: 1px #e7e7e7 solid;
|
||||
margin-right: 10px; }
|
||||
|
||||
.navbar-default .navbar-nav .current-item {
|
||||
color: #434343; }
|
||||
.navbar-default .navbar-nav .current-item:hover {
|
||||
color: #434343; }
|
||||
|
||||
.subheader {
|
||||
border-bottom: 1px #e7e7e7 solid;
|
||||
margin-bottom: 15px; }
|
||||
.subheader h2 {
|
||||
margin-bottom: 26px;
|
||||
margin-top: 26px;
|
||||
padding: 0; }
|
||||
|
||||
.post {
|
||||
margin-bottom: 50px; }
|
||||
.post img {
|
||||
max-width: 100%;
|
||||
padding: 4px;
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto; }
|
||||
|
||||
.post-image {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
display: block;
|
||||
padding: 4px;
|
||||
width: 100%; }
|
||||
.post-image div {
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
height: 250px;
|
||||
max-height: 350px; }
|
||||
|
||||
.post-header {
|
||||
margin-bottom: 10px; }
|
||||
.post-header h2 {
|
||||
margin-bottom: 0; }
|
||||
|
||||
.post-meta ul li {
|
||||
font-size: 13px;
|
||||
padding-right: 20px; }
|
||||
.post-meta a {
|
||||
color: #777; }
|
||||
|
||||
.author-image {
|
||||
background-position: center center;
|
||||
background-size: cover;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
height: 18px;
|
||||
vertical-align: top;
|
||||
width: 18px; }
|
||||
|
||||
.share {
|
||||
margin-bottom: 30px; }
|
||||
.share a {
|
||||
color: #aaa;
|
||||
transition: .5s all; }
|
||||
.share a:hover, .share a:focus {
|
||||
text-decoration: none;
|
||||
transition: .5s all; }
|
||||
.share a.share-twitter:hover {
|
||||
color: #55acee; }
|
||||
.share a.share-facebook:hover {
|
||||
color: #3b5998; }
|
||||
.share a.share-google-plus:hover {
|
||||
color: #dc4e41; }
|
||||
|
||||
.subscribe-panel {
|
||||
margin: auto;
|
||||
min-width: 300px;
|
||||
width: 30%; }
|
||||
|
||||
.site-footer {
|
||||
font-size: 13px;
|
||||
margin-bottom: 30px; }
|
||||
.site-footer hr {
|
||||
margin-bottom: 5px; }
|
||||
.site-footer p {
|
||||
margin: 0; }
|
||||
|
||||
/**************************************
|
||||
** Everything below here are my additions
|
||||
***************************************/
|
||||
|
||||
/* tabs shortcode */
|
||||
#body .nav-tabs {
|
||||
position: unset;
|
||||
width: unset;
|
||||
}
|
||||
|
||||
.tab-content > .tab-pane {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-pane {
|
||||
padding: 3px 0px;
|
||||
}
|
||||
|
||||
.tab-pane {
|
||||
padding: 0px 0px;
|
||||
}
|
||||
|
||||
.tab-content > .tab-pane.is-active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-tabs {
|
||||
color: #0f0f0f;
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nav-tabs > button {
|
||||
color: #0f0f0f;
|
||||
float: left;
|
||||
display: block;
|
||||
padding: 5px 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
padding: 9.5px;
|
||||
margin: 0px 0px 10px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.code-tabs {
|
||||
margin: 10px 0px 0px 00px;
|
||||
}
|
||||
|
||||
.nav-tabs > button.is-active, .nav-tabs > button:hover {
|
||||
color: #000 !important;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.ButtonTabs > .nav-tabs {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.ButtonTabs > .nav-tabs > .button,
|
||||
.ButtonTabs > .nav-tabs > .button.is-active {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* highlight styles */
|
||||
|
||||
.highlight {
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.highlight > pre > code {
|
||||
color: #ffffff;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.code-header {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/* header button */
|
||||
|
||||
.navbar__items > .button, .hero .button {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.nav-tabs > .button {
|
||||
padding: 2px 5px 5px 5px;
|
||||
}
|
||||
|
||||
/* misc fixes */
|
||||
|
||||
.main-wrapper {
|
||||
margin-top: 4em;
|
||||
}
|
||||
|
||||
/* .ButtonTabs > .nav-tabs > .button {
|
||||
margin-bottom: 1px;
|
||||
} */
|
||||
|
||||
section > .container > .row > .col {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.container .row .menu {
|
||||
padding: 0;
|
||||
border: solid 1px #ccc;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.hero .button {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.navbar > .navbar__inner > .navbar__items--right {
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
@media (max-width:490px) {
|
||||
.navbar > .navbar__inner > .navbar__items--left {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width:600px) {
|
||||
.navbar > .navbar__inner > .navbar__items--right > .button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.container .row .col {
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Adjusts vertical distance for microblog dynamically */
|
||||
.microblog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 80%;
|
||||
}
|
||||
|
||||
.fill-vert {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.microblog .more {
|
||||
padding-right: 1em;
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
@media (max-width:992px) {
|
||||
.side-menu {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
1794
assets/css/primer.css
Normal file
1794
assets/css/primer.css
Normal file
File diff suppressed because it is too large
Load diff
154
assets/javascripts/head.js
Normal file
154
assets/javascripts/head.js
Normal file
|
@ -0,0 +1,154 @@
|
|||
(function (i, s, o, g, r, a, m) {
|
||||
i['GoogleAnalyticsObject'] = r; i[r] = i[r] || function () {
|
||||
(i[r].q = i[r].q || []).push(arguments)
|
||||
}, i[r].l = 1 * new Date(); a = s.createElement(o),
|
||||
m = s.getElementsByTagName(o)[0]; a.async = 1; a.src = g; m.parentNode.insertBefore(a, m)
|
||||
})(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');
|
||||
|
||||
ga('create', 'UA-114468-20', 'auto');
|
||||
ga('send', 'pageview');
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
// Killing off all known SW for this site.
|
||||
caches.keys().then((cacheKeys) => Promise.all(cacheKeys.map((key) => caches.delete(key))));
|
||||
navigator.serviceWorker.getRegistrations().then(function (registrations) {
|
||||
for (let registration of registrations) {
|
||||
registration.unregister();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const deferLoadIframe = (iframe) => {
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const src = iframe.getAttribute("data-src");
|
||||
iframe.setAttribute("src", src);
|
||||
observer.unobserve(iframe);
|
||||
}
|
||||
});
|
||||
});
|
||||
observer.observe(iframe);
|
||||
}
|
||||
|
||||
window.addEventListener("load", function () {
|
||||
var iframes = document.getElementsByTagName("iframe");
|
||||
for (var i = 0; i < iframes.length; i++) {
|
||||
var ifr = iframes[i];
|
||||
if (ifr.hasAttribute("data-src")) {
|
||||
if ('IntersectionObserver' in window) {
|
||||
deferLoadIframe(ifr);
|
||||
}
|
||||
else {
|
||||
var src = ifr.getAttribute("data-src");
|
||||
ifr.setAttribute("src", src);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var shareButtons = document.querySelectorAll('div.share');
|
||||
|
||||
for (var shareButton of shareButtons) {
|
||||
shareButton.addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
var shareUrl = event.target.getAttribute('url') || '';
|
||||
var shareTitle = event.target.getAttribute('title') || '';
|
||||
if (navigator.share) {
|
||||
navigator.share({
|
||||
url: shareUrl,
|
||||
title: shareTitle,
|
||||
text: shareTitle
|
||||
}).then(function () { ga('send', 'event', 'share', 'success'); },
|
||||
function (error) { ga('send', 'event', 'share', 'error', error); });
|
||||
} else {
|
||||
var windowOptions = 'scrollbars=yes,resizable=yes,toolbar=no,location=yes,width=520,height=420';
|
||||
var twitterUrl = 'https://twitter.com/intent/tweet?text=' + encodeURIComponent(shareTitle) + '&url=' + encodeURIComponent(shareUrl) + '&via=FLEAR';
|
||||
window.open(twitterUrl, 'intent', windowOptions);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
window.onbeforeinstallprompt = function (e) {
|
||||
e.preventDefault();
|
||||
ga('send', 'event', 'install', 'prompt');
|
||||
};
|
||||
|
||||
/**********************************************************
|
||||
** Handles light/dark switcher
|
||||
***********************************************************/
|
||||
|
||||
function t(t) {
|
||||
document.documentElement.setAttribute("data-theme", t)
|
||||
}
|
||||
|
||||
function e() {
|
||||
var t = null;
|
||||
try {
|
||||
t = localStorage.getItem("theme")
|
||||
} catch (t) {}
|
||||
return t
|
||||
}
|
||||
var n = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
n.addListener((function(n) {
|
||||
null === e() && t(n.matches ? "dark" : "")
|
||||
}));
|
||||
var a = e();
|
||||
null !== a ? t(a) : n.matches && t("dark")
|
||||
|
||||
/**********************************************************
|
||||
** Handles tab short codes
|
||||
***********************************************************/
|
||||
|
||||
$(document).ready(function () {
|
||||
|
||||
$('.tab-content').find('.tab-pane').each(function (idx, item) {
|
||||
var navTabs = $(this).closest('.code-tabs').find('.nav-tabs'),
|
||||
title = $(this).attr('title');
|
||||
//navTabs.append('<li><a href="#">' + title + '</a></li');
|
||||
navTabs.append('<button class="button button--info">' + title + '</button>');
|
||||
});
|
||||
|
||||
updateCurrentTab()
|
||||
|
||||
|
||||
$('.nav-tabs button').click(function (e) {
|
||||
e.preventDefault();
|
||||
var tab = $(this),
|
||||
tabIndex = tab.index(),
|
||||
tabPanel = $(this).closest('.code-tabs'),
|
||||
tabPane = tabPanel.find('.tab-pane').eq(tabIndex);
|
||||
tabPanel.find('.is-active').removeClass('is-active');
|
||||
tab.addClass('is-active');
|
||||
tabPane.addClass('is-active');
|
||||
|
||||
// Store the number of config language selected in users' localStorage
|
||||
if(window.localStorage) {
|
||||
window.localStorage.setItem("configLangPref", tabIndex + 1)
|
||||
}
|
||||
|
||||
// After click update here not only selected part of code but others
|
||||
updateCurrentTab()
|
||||
|
||||
});
|
||||
|
||||
function updateCurrentTab() {
|
||||
var holder = '.nav-tabs button'
|
||||
|
||||
// By default current tab number is 1
|
||||
var tabNumber = 1
|
||||
|
||||
// Get saved tab number
|
||||
if (window.localStorage.getItem('configLangPref')) {
|
||||
tabNumber = window.localStorage.getItem('configLangPref')
|
||||
}
|
||||
|
||||
// Remove 'active' code to avoid multiple examples of code
|
||||
$('.nav-tabs button').closest('.code-tabs').find('.is-active').removeClass('is-active');
|
||||
|
||||
// Set 'active' state to current li(language) and div(code) by tabNumber
|
||||
$('.code-tabs div.nav-tabs').find("button:nth-of-type(" + tabNumber + ")" ).addClass('is-active');
|
||||
$('.code-tabs .tab-content').find("div:nth-of-type(" + tabNumber + ")").addClass('is-active');
|
||||
|
||||
}
|
||||
});
|
2
assets/javascripts/jquery.js
vendored
Normal file
2
assets/javascripts/jquery.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
config.json
Normal file
4
config.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"_": "This is your Now config file. For more information see the global configuration documentation: https://vercel.com/docs/configuration#global",
|
||||
"collectMetrics": true
|
||||
}
|
73
config.toml
Normal file
73
config.toml
Normal file
|
@ -0,0 +1,73 @@
|
|||
baseurl = "https://flear.org/"
|
||||
languageCode = "en-us"
|
||||
title = "Free and Libre Engineers for Amateur Radio"
|
||||
|
||||
Copyright = "FLEAR 2023"
|
||||
paginate = 20
|
||||
pygmentsCodeFences = true
|
||||
pygmentsCodeFencesGuessSyntax = true
|
||||
pygmentsStyle = "monokai"
|
||||
DefaultContentLanguage = "en"
|
||||
defaultContentLanguageInSubdir = false
|
||||
|
||||
[services.rss]
|
||||
limit = 50
|
||||
|
||||
[permalinks]
|
||||
post = "/:title/"
|
||||
|
||||
[author]
|
||||
name = "FLEAR"
|
||||
email = "flear@flear.org"
|
||||
|
||||
[params]
|
||||
author = "FLEAR"
|
||||
description = "Free and Libre Engineers for Amateur Radio"
|
||||
analyticsid = "UA-114468-20"
|
||||
|
||||
[taxonimies]
|
||||
tag = "tags"
|
||||
|
||||
[mediaTypes]
|
||||
[mediaTypes."text/javascript"]
|
||||
suffixes = ["js", "mjs"]
|
||||
[mediaTypes."application/activity+json"]
|
||||
suffixes = ["ajson"]
|
||||
|
||||
[outputFormats]
|
||||
[outputFormats.ACTIVITY]
|
||||
mediaType = "application/activity+json"
|
||||
notAlternative = true
|
||||
baseName = "activity"
|
||||
[outputFormats.ACTIVITY_OUTBOX]
|
||||
mediaType = "application/activity+json"
|
||||
notAlternative = true
|
||||
baseName = "outbox"
|
||||
|
||||
[outputs]
|
||||
home = ["HTML", "RSS", "ACTIVITY", "ACTIVITY_OUTBOX"]
|
||||
section = ["HTML", "RSS"]
|
||||
page = ["HTML"]
|
||||
|
||||
[related]
|
||||
# Only include matches with rank >= threshold. This is a normalized rank between 0 and 100.
|
||||
threshold = 10
|
||||
|
||||
# To get stable "See also" sections we, by default, exclude newer related pages.
|
||||
includeNewer = true
|
||||
|
||||
# Will lower case keywords in both queries and in the indexes.
|
||||
toLower = false
|
||||
|
||||
[[related.indices]]
|
||||
name = "tags"
|
||||
weight = 100
|
||||
|
||||
[markup.goldmark.parser]
|
||||
autoHeadingID = true
|
||||
|
||||
[markup.goldmark.extensions]
|
||||
typographer = false
|
||||
|
||||
[markup.goldmark.renderer]
|
||||
unsafe = true
|
94
content/_index.md
Normal file
94
content/_index.md
Normal file
|
@ -0,0 +1,94 @@
|
|||
---
|
||||
date: '2023-10-06T06:54:34'
|
||||
title: Welcome
|
||||
draft: false
|
||||
---
|
||||
|
||||
{{< titled-side "FLEAR" "Free and Libre Engineers for Amateur Radio" "Learn More" "/about" "Follow our GitLab →" "https://git.qoto.org/flear" >}}
|
||||
{{< example "This is the title for the example" >}}
|
||||
{{< highlight bash >}}
|
||||
console.log("hello tabs");
|
||||
System.out.println("hello tabs");
|
||||
System.out.println("hello tabs");
|
||||
System.out.println("hello tabs");
|
||||
System.out.println("hello tabs");
|
||||
System.out.println("hello tabs");
|
||||
{{< /highlight >}}
|
||||
{{< /example >}}
|
||||
{{< /titled-side >}}
|
||||
|
||||
{{< seperator >}}
|
||||
|
||||
{{< titled "For Developers" "Some description for developers" >}}
|
||||
{{< tabs "title here" >}}
|
||||
{{< tab "Java" >}}
|
||||
{{< highlight bash >}}
|
||||
System.out.println("hello tabs");
|
||||
System.out.println("hello tabs");
|
||||
System.out.println("hello tabs");
|
||||
System.out.println("hello tabs");
|
||||
{{< /highlight >}}
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab "JavaScript" >}}
|
||||
{{< highlight bash >}}
|
||||
console.log("hello tabs");
|
||||
System.out.println("hello tabs");
|
||||
System.out.println("hello tabs");
|
||||
System.out.println("hello tabs");
|
||||
System.out.println("hello tabs");
|
||||
System.out.println("hello tabs");
|
||||
{{< /highlight >}}
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
{{< /titled >}}
|
||||
|
||||
|
||||
|
||||
{{< cards "Mission statement" "Our mission to the community" >}}
|
||||
{{< card-row >}}
|
||||
{{<card "Open-source" "https://foss.com" "@digipex/digipex" "1 *" >}}
|
||||
We are commited to open source!
|
||||
{{< / card >}}
|
||||
{{<card "Open standards" "https://git.qoto.org/digipex/ax25" "@digipex/digipex" "1 *" >}}
|
||||
We are commited to open standards
|
||||
{{< / card >}}
|
||||
{{<card "Community Growth" "https://git.qoto.org/digipex/kiss-TNC" "@digipex/kiss-tnc" "1 *" >}}
|
||||
Growing the community
|
||||
{{< / card >}}
|
||||
{{< / card-row >}}
|
||||
{{< card-row >}}
|
||||
{{<card "Security" "https://git.qoto.org/digipex/apex" "@digipex/apex" "1 *" >}}
|
||||
Securing the airwaves
|
||||
{{< / card >}}
|
||||
{{<card "Preserving the Past" "https://git.qoto.org/digipex/apex" "@digipex/apex" "1 *" >}}
|
||||
Preserving historic modes and devices
|
||||
{{< / card >}}
|
||||
{{<card "Advancing Technology" "https://git.qoto.org/digipex/apex" "@digipex/apex" "1 *" >}}
|
||||
Advancing the state of HAM Radio
|
||||
{{< / card >}}
|
||||
{{< / card-row >}}
|
||||
{{< / cards >}}
|
||||
|
||||
|
||||
|
||||
{{< cards "Open source" "Watch the releases of each repo to stay up to date on new features" >}}
|
||||
{{< card-row >}}
|
||||
{{<card "Digipex" "https://git.qoto.org/digipex/digipex" "@digipex/digipex" "1 *" >}}
|
||||
An APRS and APXP implementation in Ruby
|
||||
{{< / card >}}
|
||||
{{<card "AX.25" "https://git.qoto.org/digipex/ax25" "@digipex/digipex" "1 *" >}}
|
||||
An ax.25 implementation.
|
||||
{{< / card >}}
|
||||
{{<card "KISS TNC" "https://git.qoto.org/digipex/kiss-TNC" "@digipex/kiss-tnc" "1 *" >}}
|
||||
A ruby library for KISS used on TNC.
|
||||
{{< / card >}}
|
||||
{{< / card-row >}}
|
||||
{{< card-row >}}
|
||||
{{<card "APEX" "https://git.qoto.org/digipex/apex" "@digipex/apex" "1" "*" >}}
|
||||
An APRS and APXP implementation in Pythong
|
||||
{{< / card >}}
|
||||
{{< / card-row >}}
|
||||
{{< / cards >}}
|
||||
|
||||
{{< info-buttons "Learn More" "/about" "Follow our GitLab →" "https://git.qoto.org/flear" >}}
|
17
content/news/2023-10-05-activitypub-support-added.md
Normal file
17
content/news/2023-10-05-activitypub-support-added.md
Normal file
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
slug: activitypub-support-added
|
||||
date: '2023-10-05T22:36:20.122Z'
|
||||
title: ActivityPub Support Added
|
||||
tags:
|
||||
- administration
|
||||
draft: false
|
||||
---
|
||||
|
||||
We have added ActivityPub support for this website!
|
||||
|
||||
If you would like to follow this blog from Mastodon or anywhere in the Fediverse
|
||||
just follow the handle `@flear@flear.org` and you will get new posts to this
|
||||
site directly in your feed.
|
||||
|
||||
In addition if you comment on one of our posts directly from your feed your
|
||||
comments and likes will show on the page for that article.
|
12
content/news/2023-10-05-flear-goes-live.md
Normal file
12
content/news/2023-10-05-flear-goes-live.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
slug: flear-goes-live
|
||||
date: '2023-10-05T02:44:49'
|
||||
title: FLEAR Goes Live
|
||||
tags:
|
||||
- administration
|
||||
draft: false
|
||||
---
|
||||
|
||||
FLEAR has officially launched!
|
||||
|
||||
More news to come soon.
|
10
content/news/2023-10-05-sending-notes-added.md
Normal file
10
content/news/2023-10-05-sending-notes-added.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
slug: sending-notes-added
|
||||
date: '2023-10-05T15:27:55'
|
||||
title: Sending Notes Added
|
||||
tags:
|
||||
- administration
|
||||
draft: false
|
||||
---
|
||||
|
||||
We have now added full support for sending notes when new posts are availible!
|
9
content/projects/digipex.markdown
Normal file
9
content/projects/digipex.markdown
Normal file
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
slug: digipex
|
||||
date: '2023-10-05T22:36:20.122Z'
|
||||
title: "Digipex"
|
||||
tags: ['APRS', 'APXP']
|
||||
description: "A next-generation APRS suite implementing APXP."
|
||||
---
|
||||
|
||||
Information on the Digipex project comming soon.
|
63
content/resource/entry/qoto.md
Normal file
63
content/resource/entry/qoto.md
Normal file
|
@ -0,0 +1,63 @@
|
|||
---
|
||||
title: QOTO
|
||||
date: '2023-10-05T22:36:10.122Z'
|
||||
type: entry
|
||||
slug: qoto
|
||||
tags: ['Incubator']
|
||||
---
|
||||
|
||||
QOTO aims to provide a community where our users do not fear being punished for their personal opinions. We do not allow people to disseminate ideologies that are abusive or violent towards others. Demonstrating support for or defending ideologies known to be violent or hateful is a bannable offense. This includes, but is not limited to: racial supremacy, anti-LGBTQ or anti-cis-gender/anti-straight, pro-genocide, child abuse or child pornography, etc. While we recognize questions and conversation regarding these topics are essential for a STEM community, in general, doing so in bad faith will result in immediate expulsion.
|
||||
|
||||
All discussions between mods are public, as we believe that transparency is the best way to show everyone how we put words into practice.
|
||||
|
||||
Additional services:
|
||||
|
||||
* [QOTO Groups](https://groups.qoto.org) - a server devoted to moderated groups on the Fediverse.
|
||||
* [NextCloud](https://cloud.qoto.org)
|
||||
* [GitLab](https://git.qoto.org)
|
||||
* GitLab pages (including user hosted pages) - https://*.qoto.io
|
||||
* [FunkWhale](https://audio.qoto.org)
|
||||
* [PeerTube](https://video.qoto.org)
|
||||
* [Discourse](https://discourse.qoto.org)
|
||||
* [Matrix chat](https://element.qoto.org)
|
||||
|
||||
|
||||
Unique Features
|
||||
|
||||
* Inline math Latex support - Use \ ( and \ ) for inline LaTeX, and \ [ and \ ] for display mode.
|
||||
* 65,535 character limit for toots (usually 500)
|
||||
* 65,535 character limit for profile bio (usually 160)
|
||||
* Full text searches - usually you can only search hashtags and usernames
|
||||
* Groups Support - Accounts which represent groups (such as those from Guppe) are now tagged with a group badge and are specially rendered so the messages relayed by a group appear to be originating from the user.
|
||||
* Dedicated Groups Server - The fediverses only moderated groups server open to the public.
|
||||
* Group Directory - Groups are federated in the group directory which, like the federated timeline, will list all groups followed by anyone on our server.
|
||||
* Markdown Posts - You now have the option to select markdown formatting when posting.
|
||||
* Circles - You can now create circles with followers in it. When you make a post you can select one or more circles to privately post to and only users of those circles can see your post.
|
||||
* Per-toot option allowing for local-only toots that wont federate with other servers
|
||||
* Local-only toot default - You can now optionally set all toots to be local-only and prevent all your posts from federating outside of the local server. Without this setting you can still select local-only on a per-toot basis.
|
||||
* Domain Subscriptions - Bring another instance's local timeline as a feed in Qoto, no need to have an account on another server again, just import their timeline here!
|
||||
* Favorite Domains - Our last release allowed us to add remote timeline as a feature where you could pull up the local timeline of a remote instance as its own column. Now you can also add domains to a favorite domain list which makes it easier to access and bring up the timelines by listing them directly in the navigation panel.
|
||||
* User Post Notification - Users now have a bell next to their name where the follow button is. This will send you a notification every time they makes a new post.
|
||||
* Keyword Subscriptions - Create custom timelines based off keywords not just hashtags, you can even use regex for advanced matching
|
||||
* Account Subscriptions - This allows you to follow a remote accounts public toots in your Home timeline without actually following them. NOTE: This will not circumvent a user's privacy settings. It only delivers the same notifications as following someone via existing RSS feeds would allow, that is, it brings the RSS feed of their public posts into your home timeline this features behaves the same as the default functionality on non-Mastodon instances such as Misskey, Friendica, and Pleroma.
|
||||
* Misskey compatible quoting of toots
|
||||
* Colored follow button - The follow button seen in the feed and in profiles (the one that can be optionally turned off in settings) will now be colored to indicate follower status. Gray if not followed, yellow if followed, blue if following, green if mutual follow
|
||||
* Bookmarking of toots
|
||||
* Support for WebP and HEIC media uploads
|
||||
* Optional instance ticker banners under usernames in the timelines that tell you what server a user is on and what software it is running (ie pleorma, mastodon, misskey, etc) at a glance.
|
||||
* Optionally display subscribe and follower buttons directly in your timeline feeds.
|
||||
* Remote Timeline Quickview - From any post or profile there is a link to bring up the remote timeline locally, right in QOTO without needing to open a new window.
|
||||
* Professionally hosted with nightly backups
|
||||
* light modern theme with full width columns (not fixed)
|
||||
* Several extra themes - including mastodon default and mastodon default with full width columns (not fixed)
|
||||
* Read/unread notifications - Unread notifications now have a subtle blue indicator. There is also a “mark all as red” button added.
|
||||
* Rich-text rendering - We now render rich-text from other servers, so servers which post in a formatting that includes things like bolding, quotes, headers, italics, underlines, etc, will render in the statuses.
|
||||
* Rich-text Stripping - The user option to partially or fully turn off the rendering of rich-text.
|
||||
* Registration Captcha - A captcha is now required to signup for a new account, reducing spam.
|
||||
* Quote feature opt-out - As introduced earlier you can quote a toot rather than just reboost it. This is an additional button on every toot. This can now be turned off in your settings and removes the button.
|
||||
* Bookmark feature opt-out - As with the quote feature we could previously bookmark posts. You can now turn off the bookmark button.
|
||||
* Follow button opt-out - As mentioned earlier our added feature allowing you to add a follow button, optionally colored, into the feed can now be turned off.
|
||||
* Subscribe button opt-out - As before the subscribe feature that lets you watch a users public toots without following them can now be optionally turned off.
|
||||
* Favorite Tags - Similar to favorite domains this allows you to create a list of your favorite tags and have them easily accessible in the navigation panel.
|
||||
|
||||
[See more here](https://qoto.org/about/more)
|
16
content/resource/qoto.md
Normal file
16
content/resource/qoto.md
Normal file
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
title: QOTO Incubator Available for FLEAR
|
||||
date: '2023-10-07T23:27:52+00:00'
|
||||
slug: qoto-incubator-available
|
||||
type: resource
|
||||
tags: ['Announcements', 'Incubator']
|
||||
---
|
||||
|
||||
For the time being we will use [https://qoto.org](https://qoto.org) to provide
|
||||
generic resources such as git, web hosting, and other services. They are an
|
||||
open-source community servicing the entire STEM community.
|
||||
|
||||
As we slowly ramp up we may begin to move services over to our own domain and
|
||||
servers. Stay tuned.
|
||||
|
||||
See more about [QOTO here](../entry/qoto).
|
20
generateKeys.mts
Normal file
20
generateKeys.mts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { promisify } from 'util';
|
||||
import { generateKeyPair } from 'crypto';
|
||||
|
||||
const generateKeyPairAsync = promisify(generateKeyPair);
|
||||
|
||||
const pair = await generateKeyPairAsync('rsa', {
|
||||
modulusLength: 4096,
|
||||
publicKeyEncoding: {
|
||||
type: 'spki',
|
||||
format: 'pem'
|
||||
},
|
||||
privateKeyEncoding: {
|
||||
type: 'pkcs8',
|
||||
format: 'pem'
|
||||
}
|
||||
})
|
||||
|
||||
console.log(pair.publicKey);
|
||||
|
||||
console.log(pair.privateKey);
|
76
layouts/_default/list.html
Normal file
76
layouts/_default/list.html
Normal file
|
@ -0,0 +1,76 @@
|
|||
{{ partial "top_list_generic.html" . }}
|
||||
|
||||
{{ $paginator := .Paginate ( where .Pages ".Draft" "!=" true )}}
|
||||
<div class='row'>
|
||||
<div class='col-md-8'>
|
||||
<div class='extra-pagination inner'>
|
||||
<nav class='pagination' role='pagination'>
|
||||
{{ if .Paginator.HasNext }}<a href="{{ .Paginator.Next.URL }}" class="newer-posts">← Newer Posts</a>{{ end }}
|
||||
<span class='page-number'>Page {{ .Paginator.PageNumber }} of {{ .Paginator.TotalPages }}</span>
|
||||
{{ if .Paginator.HasPrev }}<a href="{{ .Paginator.Prev.URL }}" class="older-posts">Older Posts →</a>{{ end }}
|
||||
</nav>
|
||||
</div>
|
||||
{{ range $index, $element := $paginator.Pages }}
|
||||
|
||||
<article class='post'>
|
||||
<header class='post-header'>
|
||||
<h2 class='post-title'>
|
||||
<a href="{{ .Permalink }}">{{ .Title}}</a>
|
||||
{{ if .Params.link }} — <a href="{{.Params.link}}">🔗</a>{{end}}
|
||||
</h2>
|
||||
<section class='post-meta text-muted'>
|
||||
<ul class='list-inline'>
|
||||
<li>
|
||||
<i class='fa fa-calendar'></i>
|
||||
<time class='post-date' datetime='{{ .Date.Format "2020-09-10" }}'> {{ .Date.Format "January 2 2006" }} </time>
|
||||
</li>
|
||||
<li>
|
||||
<a class='fa-solid fa-book-open' href='{{ .Permalink }}'>
|
||||
<span class='hidden'>Book icon</span>
|
||||
</a>
|
||||
<a href='{{ .Permalink }}'>Reading time: {{ .ReadingTime }} minute{{ if (ne .ReadingTime 1) }}s{{ end }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<i class='fa fa-folder-open'></i>
|
||||
{{ $tags := .Params.tags}}
|
||||
{{ with $tags }}
|
||||
{{ range . }}
|
||||
<a href="{{ "/tags/" | relLangURL }}{{ . | urlize }}">{{ . }}</a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</header>
|
||||
<section class='post-content'>
|
||||
{{.Content }}
|
||||
</section>
|
||||
</article>
|
||||
|
||||
{{ end }}
|
||||
|
||||
<div class='extra-pagination inner'>
|
||||
<nav class='pagination' role='pagination'>
|
||||
{{ if .Paginator.HasNext }}<a href="{{ .Paginator.Next.URL }}" class="newer-posts">← Newer Posts</a>{{ end }}
|
||||
<span class='page-number'>Page {{ .Paginator.PageNumber }} of {{ .Paginator.TotalPages }}</span>
|
||||
{{ if .Paginator.HasPrev }}<a href="{{ .Paginator.Prev.URL }}" class="older-posts">Older Posts →</a>{{ end }}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class='col-md-4'>
|
||||
<nav class='menu tags'>
|
||||
<span class="menu-heading">Categories</span>
|
||||
<a class='menu-item selected' data-slug='all' href='#'>All posts</a>
|
||||
{{range $name, $taxonomy := .Site.Taxonomies.tags}} {{ $cnt := .Count }}
|
||||
{{ with $.Site.GetPage (printf "/tags/%s" $name) }}
|
||||
<a href="{{ .RelPermalink }}" class='menu-item tag' data-slug='{{$name}}' title="All pages with tag <i>{{$name}}</i>">{{$name}} ({{$cnt}})</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</nav>
|
||||
<br>
|
||||
{{ partial "microblog.html" . }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ partial "bottom_list.html" . }}
|
45
layouts/_default/rss.xml
Normal file
45
layouts/_default/rss.xml
Normal file
|
@ -0,0 +1,45 @@
|
|||
{{- $pctx := . -}}
|
||||
{{- if .IsHome -}}{{ $pctx = .Site }}{{- end -}}
|
||||
{{- $pages := slice -}}
|
||||
{{- if or $.IsHome $.IsSection -}}
|
||||
{{- $pages = $pctx.RegularPages -}}
|
||||
{{- else -}}
|
||||
{{- $pages = $pctx.Pages -}}
|
||||
{{- end -}}
|
||||
{{- $limit := .Site.Config.Services.RSS.Limit -}}
|
||||
{{- if ge $limit 1 -}}
|
||||
{{- $pages = $pages | first $limit -}}
|
||||
{{- end -}}
|
||||
{{- printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" | safeHTML }}
|
||||
<rss version="2.0"
|
||||
xmlns:atom="http://www.w3.org/2005/Atom"
|
||||
xmlns:content="http://purl.org/rss/1.0/modules/content/">
|
||||
<channel>
|
||||
<title>{{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} on {{ end }}{{ .Site.Title }}{{ end }}</title>
|
||||
<link>{{ .Permalink }}</link>
|
||||
<description>Recent content {{ if ne .Title .Site.Title }}{{ with .Title }}in {{.}} {{ end }}{{ end }}on {{ .Site.Title }}</description>
|
||||
<generator>Hugo -- gohugo.io</generator>{{ with .Site.LanguageCode }}
|
||||
<language>{{.}}</language>{{end}}{{ with .Site.Author.email }}
|
||||
<managingEditor>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</managingEditor>{{end}}{{ with .Site.Author.email }}
|
||||
<webMaster>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</webMaster>{{end}}{{ with .Site.Copyright }}
|
||||
<copyright>{{.}}</copyright>{{end}}{{ if not .Date.IsZero }}
|
||||
<lastBuildDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</lastBuildDate>{{ end }}
|
||||
{{ with .OutputFormats.Get "RSS" }}
|
||||
{{ printf "<atom:link href=%q rel=\"self\" type=%q />" .Permalink .MediaType | safeHTML }}
|
||||
{{ end }}
|
||||
{{ $notdrafts := where $pages ".Draft" "!=" true }}
|
||||
{{ range (where $notdrafts "Type" "in" (slice "resource" "post" "page")) }}
|
||||
<item>
|
||||
<title>{{ .Title }}</title>
|
||||
<link>{{ .Permalink }}</link>
|
||||
<pubDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</pubDate>
|
||||
{{ with .Site.Author.email }}<author>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</author>{{end}}
|
||||
<guid>{{ .Permalink }}</guid>
|
||||
<description>{{ .Summary | html }}</description>
|
||||
<content:encoded> {{ printf "<![CDATA[" | safeHTML }}
|
||||
{{- printf "%s" .Content | safeHTML }}
|
||||
{{- printf "]]>" | safeHTML }}</content:encoded>
|
||||
</item>
|
||||
{{ end }}
|
||||
</channel>
|
||||
</rss>
|
67
layouts/_default/single.html
Normal file
67
layouts/_default/single.html
Normal file
|
@ -0,0 +1,67 @@
|
|||
{{ partial "top_single.html" . }}
|
||||
<div class='row'>
|
||||
<div class='col-md-8'>
|
||||
<article class='post'>
|
||||
<header class='post-header'>
|
||||
<section class='post-meta text-muted'>
|
||||
<ul class='list-inline'>
|
||||
<li>
|
||||
<i class='fa fa-calendar'></i>
|
||||
<time class='post-date' datetime='{{ .Date.Format "2020-09-10" }}'> {{ .Date.Format "January 2 2006" }} </time>
|
||||
</li>
|
||||
<li>
|
||||
<a class='fa-solid fa-book-open' href='{{ .Permalink }}'>
|
||||
<span class='hidden'>Book icon</span>
|
||||
</a>
|
||||
<a href='{{ .Permalink }}'>Reading time: {{ .ReadingTime }} minute{{ if (ne .ReadingTime 1) }}s{{ end }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<i class='fa fa-folder-open'></i>
|
||||
{{ $tags := .Params.tags}}
|
||||
{{ with $tags }}
|
||||
{{ range . }}
|
||||
<a href="{{ "/tags/" | relLangURL }}{{ . | urlize }}">{{ . }}</a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</header>
|
||||
<section class='post-content'>
|
||||
{{ if isset .Params "toc" }}{{ .TableOfContents }}{{ end}}
|
||||
{{ .Content }}
|
||||
{{ partial "article_footer.html" . }}
|
||||
{{ partial "comments.html" . }}
|
||||
<section class='share text-center'>
|
||||
<a class='share-twitter' href='https://twitter.com/intent/tweet?text={{ if .Title }}{{ .Title }}{{else}}{{.Site.Title}}{{end}}amp;url={{ .Permalink }}' onclick='window.open(this.href, 'twitter-share', 'width=550,height=235');return false;' true=''>
|
||||
<span class='fa-stack fa-lg'>
|
||||
<i class='fa fa-circle fa-stack-2x'></i>
|
||||
<i class='fa-brands fa-twitter fa-stack-1x fa-inverse'></i>
|
||||
</span>
|
||||
</a>
|
||||
<a class='share-facebook' href='https://www.facebook.com/sharer/sharer.php?u={{ .Permalink }}' onclick='window.open(this.href, 'facebook-share','width=580,height=296');return false;' true=''>
|
||||
<span class='fa-stack fa-lg'>
|
||||
<i class='fa fa-circle fa-stack-2x'></i>
|
||||
<i class='fa-brands fa-facebook-f fa-stack-1x fa-inverse'></i>
|
||||
</span>
|
||||
</a>
|
||||
</section>
|
||||
{{ partial "comments.html" . }}
|
||||
</section>
|
||||
</article>
|
||||
</div>
|
||||
<div class='col-md-4'>
|
||||
<nav class='menu tags'>
|
||||
<span class="menu-heading">Recent Posts</span>
|
||||
{{ $hereLink := .Permalink }}
|
||||
{{ range .Site.Pages }}
|
||||
{{ range .Pages }}
|
||||
<a href='{{ .RelPermalink }}' class='menu-item tag{{ if (eq .Permalink $hereLink) }} selected{{ end }}' data-slug='{{ .Title}}' title="{{ .Title}}">{{ .Title}}</a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</nav>
|
||||
<br>
|
||||
{{ partial "microblog.html" . }}
|
||||
</div>
|
||||
</div>
|
||||
{{ partial "bottom_single.html" . }}
|
30
layouts/_default/terms.html
Normal file
30
layouts/_default/terms.html
Normal file
|
@ -0,0 +1,30 @@
|
|||
{{ partial "head.html" . }}
|
||||
|
||||
<body class="list">
|
||||
<header class="me">
|
||||
<div>
|
||||
<img src="/images/logo.png">
|
||||
<h1>Hello.</h1>
|
||||
<h2>I am FLEAR.</h2>
|
||||
<h3>A Developer Advocate for Chrome and the Open Web at Google.</h3>
|
||||
</div>
|
||||
</header>
|
||||
<div>
|
||||
<main>
|
||||
{{ $data := .Data }}
|
||||
{{ range $key,$value := .Data.Terms.ByCount }}
|
||||
<div class="post">
|
||||
<header>
|
||||
<h3><a href="/{{ $data.Plural }}/{{ $value.Name | urlize }}">{{ $value.Name }}</a> ({{ $value.Count }} articles)</h3>
|
||||
</header>
|
||||
<div class="description">
|
||||
{{ range $key1,$page := $value.WeightedPages }}
|
||||
<li><a href="{{$page.Permalink}}">{{ $page.Title }}</a> - published <em>{{ .Date.Format "January 2 2006" }}</em></li>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
69
layouts/entry/single.html
Normal file
69
layouts/entry/single.html
Normal file
|
@ -0,0 +1,69 @@
|
|||
|
||||
{{ partial "top_single.html" . }}
|
||||
<div class='row'>
|
||||
<div class='col-md-8'>
|
||||
<article class='post'>
|
||||
<header class='post-header'>
|
||||
<section class='post-meta text-muted'>
|
||||
<ul class='list-inline'>
|
||||
<li>
|
||||
<i class='fa fa-calendar'></i>
|
||||
<time class='post-date' datetime='{{ .Date.Format "2020-09-10" }}'> {{ .Date.Format "January 2 2006" }} </time>
|
||||
</li>
|
||||
<li>
|
||||
<a class='fa-solid fa-book-open' href='{{ .Permalink }}'>
|
||||
<span class='hidden'>Book icon</span>
|
||||
</a>
|
||||
<a href='{{ .Permalink }}'>Reading time: {{ .ReadingTime }} minute{{ if (ne .ReadingTime 1) }}s{{ end }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<i class='fa fa-folder-open'></i>
|
||||
{{ $tags := .Params.tags}}
|
||||
{{ with $tags }}
|
||||
{{ range . }}
|
||||
<a href="{{ "/tags/" | relLangURL }}{{ . | urlize }}">{{ . }}</a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</header>
|
||||
<section class='post-content'>
|
||||
{{ if isset .Params "toc" }}{{ .TableOfContents }}{{ end}}
|
||||
{{ .Content }}
|
||||
{{ partial "backlinks.html" . }}
|
||||
{{ partial "article_footer.html" . }}
|
||||
<section class='share text-center'>
|
||||
<a class='share-twitter' href='https://twitter.com/intent/tweet?text={{ if .Title }}{{ .Title }}{{else}}{{.Site.Title}}{{end}}amp;url={{ .Permalink }}' onclick='window.open(this.href, 'twitter-share', 'width=550,height=235');return false;' true=''>
|
||||
<span class='fa-stack fa-lg'>
|
||||
<i class='fa fa-circle fa-stack-2x'></i>
|
||||
<i class='fa-brands fa-twitter fa-stack-1x fa-inverse'></i>
|
||||
</span>
|
||||
</a>
|
||||
<a class='share-facebook' href='https://www.facebook.com/sharer/sharer.php?u={{ .Permalink }}' onclick='window.open(this.href, 'facebook-share','width=580,height=296');return false;' true=''>
|
||||
<span class='fa-stack fa-lg'>
|
||||
<i class='fa fa-circle fa-stack-2x'></i>
|
||||
<i class='fa-brands fa-facebook-f fa-stack-1x fa-inverse'></i>
|
||||
</span>
|
||||
</a>
|
||||
</section>
|
||||
{{ partial "comments.html" . }}
|
||||
</section>
|
||||
</article>
|
||||
</div>
|
||||
<div class='col-md-4'>
|
||||
<nav class='menu tags'>
|
||||
<span class="menu-heading">Recent Resource Posts</span>
|
||||
<a class='menu-item' data-slug='all' href='/resource'>All posts</a>
|
||||
{{ $hereLink := .Permalink }}
|
||||
{{ range (where .Site.Pages "Section" "resource") }}
|
||||
{{ range .Pages }}
|
||||
<a href='{{ .RelPermalink }}' class='menu-item tag{{ if (eq .Permalink $hereLink) }} selected{{ end }}' data-slug='{{ .Title}}' title="{{ .Title}}">{{ .Title}}</a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</nav>
|
||||
<br>
|
||||
{{ partial "microblog.html" . }}
|
||||
</div>
|
||||
</div>
|
||||
{{ partial "bottom_single.html" . }}
|
16
layouts/index.activity.ajson
Normal file
16
layouts/index.activity.ajson
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"@context": ["https://www.w3.org/ns/activitystreams",
|
||||
{"@language": ""en-GB"}],
|
||||
"type": "Person",
|
||||
"id": "{{ $.Site.BaseURL }}",
|
||||
"outbox": "{{ $.Site.BaseURL }}outbox",
|
||||
"inbox": "{{ $.Site.BaseURL }}inbox",
|
||||
"preferredUsername": "{{$.Site.Author.name}}",
|
||||
"name": "{{$.Site.Author.name}} - {{$.Site.Title}}",
|
||||
"summary": "{{$.Site.Params.Description}}",
|
||||
"icon": {
|
||||
"type":"Image",
|
||||
"mediaType":"image/png",
|
||||
"url": "{{ $.Site.BaseURL }}images/logo.png"
|
||||
}
|
||||
}
|
41
layouts/index.activity_outbox.ajson
Normal file
41
layouts/index.activity_outbox.ajson
Normal file
|
@ -0,0 +1,41 @@
|
|||
{{- $pctx := . -}}
|
||||
{{- if .IsHome -}}{{ $pctx = .Site }}{{- end -}}
|
||||
{{- $pages := slice -}}
|
||||
{{- if or $.IsHome $.IsSection -}}
|
||||
{{- $pages = $pctx.RegularPages -}}
|
||||
{{- else -}}
|
||||
{{- $pages = $pctx.Pages -}}
|
||||
{{- end -}}
|
||||
{{- $limit := .Site.Config.Services.RSS.Limit -}}
|
||||
{{- if ge $limit 1 -}}
|
||||
{{- $pages = $pages | first $limit -}}
|
||||
{{- end -}}
|
||||
{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"id": "{{ $.Site.BaseURL }}outbox",
|
||||
"summary": "{{$.Site.Author.name}} - {{$.Site.Title}}",
|
||||
"type": "OrderedCollection",
|
||||
{{ $notdrafts := where $pages ".Draft" "!=" true }}
|
||||
{{ $all := where $notdrafts "Type" "in" (slice "resource" "post" "page")}}
|
||||
"totalItems": {{(len $all)}},
|
||||
"orderedItems": [
|
||||
{{ range $index, $element := $all }}
|
||||
{{- if ne $index 0 }}, {{ end }}
|
||||
{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"id": "{{.Permalink}}-create",
|
||||
"type": "Create",
|
||||
"actor": "https://flear.org/flear",
|
||||
"object": {
|
||||
"id": "{{ .Permalink }}",
|
||||
"type": "Note",
|
||||
"content": "{{.Title}}<br><a href='{{.Permalink}}'>{{.Permalink}}</a>",
|
||||
"url": "{{.Permalink}}",
|
||||
"attributedTo": "https://flear.org/flear",
|
||||
"to": "https://www.w3.org/ns/activitystreams#Public",
|
||||
"published": {{ dateFormat "2006-01-02T15:04:05-07:00" .Date | jsonify }}
|
||||
}
|
||||
}
|
||||
{{end}}
|
||||
]
|
||||
}
|
3
layouts/index.html
Normal file
3
layouts/index.html
Normal file
|
@ -0,0 +1,3 @@
|
|||
{{ partial "top_home.html" }}
|
||||
{{ .Content }}
|
||||
{{ partial "bottom_home.html" }}
|
91
layouts/news/list.html
Normal file
91
layouts/news/list.html
Normal file
|
@ -0,0 +1,91 @@
|
|||
{{ partial "top_list_news.html" . }}
|
||||
|
||||
{{ $paginator := .Paginate (where .Data.Pages ".Section" "news") }}
|
||||
<div class='row'>
|
||||
<div class='col-md-8'>
|
||||
<div class='extra-pagination inner'>
|
||||
<nav class='pagination' role='pagination'>
|
||||
{{ if .Paginator.HasNext }}<a href="{{ .Paginator.Next.URL }}" class="newer-posts">← Newer Posts</a>{{ end }}
|
||||
<span class='page-number'>Page {{ .Paginator.PageNumber }} of {{ .Paginator.TotalPages }}</span>
|
||||
{{ if .Paginator.HasPrev }}<a href="{{ .Paginator.Prev.URL }}" class="older-posts">Older Posts →</a>{{ end }}
|
||||
</nav>
|
||||
</div>
|
||||
{{ range $index, $element := $paginator.Pages }}
|
||||
|
||||
<article class='post'>
|
||||
<header class='post-header'>
|
||||
<h2 class='post-title'>
|
||||
<a href="{{ .Permalink }}">{{ .Title}}</a>
|
||||
{{ if .Params.link }} — <a href="{{.Params.link}}">🔗</a>{{end}}
|
||||
</h2>
|
||||
<section class='post-meta text-muted'>
|
||||
<ul class='list-inline'>
|
||||
<li>
|
||||
<i class='fa fa-calendar'></i>
|
||||
<time class='post-date' datetime='{{ .Date.Format "2020-09-10" }}'> {{ .Date.Format "January 2 2006" }} </time>
|
||||
</li>
|
||||
<li>
|
||||
<a class='fa-solid fa-book-open' href='{{ .Permalink }}'>
|
||||
<span class='hidden'>Book icon</span>
|
||||
</a>
|
||||
<a href='{{ .Permalink }}'>Reading time: {{ .ReadingTime }} minute{{ if (ne .ReadingTime 1) }}s{{ end }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<i class='fa fa-folder-open'></i>
|
||||
{{ $tags := .Params.tags}}
|
||||
{{ with $tags }}
|
||||
{{ range . }}
|
||||
<a href="{{ "/tags/" | relLangURL }}{{ . | urlize }}">{{ . }}</a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</header>
|
||||
<section class='post-content'>
|
||||
{{.Content }}
|
||||
</section>
|
||||
</article>
|
||||
|
||||
{{ end }}
|
||||
|
||||
<div class='extra-pagination inner'>
|
||||
<nav class='pagination' role='pagination'>
|
||||
{{ if .Paginator.HasNext }}<a href="{{ .Paginator.Next.URL }}" class="newer-posts">← Newer Posts</a>{{ end }}
|
||||
<span class='page-number'>Page {{ .Paginator.PageNumber }} of {{ .Paginator.TotalPages }}</span>
|
||||
{{ if .Paginator.HasPrev }}<a href="{{ .Paginator.Prev.URL }}" class="older-posts">Older Posts →</a>{{ end }}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class='col-md-4'>
|
||||
<nav class='menu tags'>
|
||||
<span class="menu-heading">News Categories</span>
|
||||
<a class='menu-item selected' data-slug='all' href='/news'>All posts</a>
|
||||
{{ $hereSection := .Section }}
|
||||
{{/* Get current taxonomy (example: tags, categories). */}}
|
||||
<!-- {{ $taxonomy := .Data.Plural }} -->
|
||||
{{ $taxonomy := "tags" }}
|
||||
{{/* Get sections where one or more pages has one or more terms in the current taxonomy. */}}
|
||||
{{ $sections := slice }}
|
||||
{{ range $term, $weightedPages := index site.Taxonomies $taxonomy }}
|
||||
{{ range $weightedPages }}
|
||||
{{ $sections = $sections | append .CurrentSection | uniq | sort }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{/* List pages by term by section. */}}
|
||||
{{ range $section := (where $sections ".Path" $hereSection) }}
|
||||
{{ range $term, $weightedPages := index site.Taxonomies $taxonomy }}
|
||||
{{ $termPage := site.GetPage path.Join $taxonomy $term }}
|
||||
{{ with where $weightedPages "Section" $section.Path }}
|
||||
<a href="{{ $termPage.RelPermalink }}" class='menu-item tag' data-slug='{{ $termPage.LinkTitle }}' title="All pages with tag <i>{{ $termPage.LinkTitle }}</i>">{{ $termPage.LinkTitle }}</a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</nav>
|
||||
<br>
|
||||
{{ partial "microblog.html" . }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ partial "bottom_list.html" . }}
|
67
layouts/news/single.html
Normal file
67
layouts/news/single.html
Normal file
|
@ -0,0 +1,67 @@
|
|||
{{ partial "top_single.html" . }}
|
||||
<div class='row'>
|
||||
<div class='col-md-8'>
|
||||
<article class='post'>
|
||||
<header class='post-header'>
|
||||
<section class='post-meta text-muted'>
|
||||
<ul class='list-inline'>
|
||||
<li>
|
||||
<i class='fa fa-calendar'></i>
|
||||
<time class='post-date' datetime='{{ .Date.Format "2020-09-10" }}'> {{ .Date.Format "January 2 2006" }} </time>
|
||||
</li>
|
||||
<li>
|
||||
<a class='fa-solid fa-book-open' href='{{ .Permalink }}'>
|
||||
<span class='hidden'>Book icon</span>
|
||||
</a>
|
||||
<a href='{{ .Permalink }}'>Reading time: {{ .ReadingTime }} minute{{ if (ne .ReadingTime 1) }}s{{ end }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<i class='fa fa-folder-open'></i>
|
||||
{{ $tags := .Params.tags}}
|
||||
{{ with $tags }}
|
||||
{{ range . }}
|
||||
<a href="{{ "/tags/" | relLangURL }}{{ . | urlize }}">{{ . }}</a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</header>
|
||||
<section class='post-content'>
|
||||
{{ if isset .Params "toc" }}{{ .TableOfContents }}{{ end}}
|
||||
{{ .Content }}
|
||||
{{ partial "article_footer.html" . }}
|
||||
<section class='share text-center'>
|
||||
<a class='share-twitter' href='https://twitter.com/intent/tweet?text={{ if .Title }}{{ .Title }}{{else}}{{.Site.Title}}{{end}}amp;url={{ .Permalink }}' onclick='window.open(this.href, 'twitter-share', 'width=550,height=235');return false;' true=''>
|
||||
<span class='fa-stack fa-lg'>
|
||||
<i class='fa fa-circle fa-stack-2x'></i>
|
||||
<i class='fa-brands fa-twitter fa-stack-1x fa-inverse'></i>
|
||||
</span>
|
||||
</a>
|
||||
<a class='share-facebook' href='https://www.facebook.com/sharer/sharer.php?u={{ .Permalink }}' onclick='window.open(this.href, 'facebook-share','width=580,height=296');return false;' true=''>
|
||||
<span class='fa-stack fa-lg'>
|
||||
<i class='fa fa-circle fa-stack-2x'></i>
|
||||
<i class='fa-brands fa-facebook-f fa-stack-1x fa-inverse'></i>
|
||||
</span>
|
||||
</a>
|
||||
</section>
|
||||
{{ partial "comments.html" . }}
|
||||
</section>
|
||||
</article>
|
||||
</div>
|
||||
<div class='col-md-4'>
|
||||
<nav class='menu tags'>
|
||||
<span class="menu-heading">Recent News Posts</span>
|
||||
<a class='menu-item' data-slug='all' href='/news'>All posts</a>
|
||||
{{ $hereLink := .Permalink }}
|
||||
{{ range (where .Site.Pages "Section" "news") }}
|
||||
{{ range .Pages }}
|
||||
<a href='{{ .RelPermalink }}' class='menu-item tag{{ if (eq .Permalink $hereLink) }} selected{{ end }}' data-slug='{{ .Title}}' title="{{ .Title}}">{{ .Title}}</a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</nav>
|
||||
<br>
|
||||
{{ partial "microblog.html" . }}
|
||||
</div>
|
||||
</div>
|
||||
{{ partial "bottom_single.html" . }}
|
18
layouts/partials/article_footer.html
Normal file
18
layouts/partials/article_footer.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
{{ $related := .Site.RegularPages.Related . | first 5 }}
|
||||
{{ with $related }}
|
||||
<div class="related pages">
|
||||
<h4>See Also</h4>
|
||||
<ul>
|
||||
{{ range . }}
|
||||
<li><a href="{{ .RelPermalink }}">{{ .Title }}</a></li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
{{ end }}
|
||||
<div class="widgets source">
|
||||
<a href="https://git.qoto.org/flear/flear-site/tree/master/content/{{.File.LogicalName}}" rel="nofollow">View Source</a>
|
||||
|
|
||||
<a href="https://git.qoto.org/flear/flear-site/edit/master/content/{{.File.LogicalName}}" rel="nofollow">Make a correction</a>
|
||||
|
|
||||
<a href="https://git.qoto.org/flear/flear-site/commits/master/content/{{.File.LogicalName}}" rel="nofollow">Correction history</a>
|
||||
</div>
|
31
layouts/partials/backlinks.html
Normal file
31
layouts/partials/backlinks.html
Normal file
|
@ -0,0 +1,31 @@
|
|||
{{ $re := $.File.BaseFileName }} {{ $backlinks := slice }} {{ range where
|
||||
.Site.RegularPages "Type" "in" (slice "resource" "entry") }} {{ if and (findRE $re .RawContent) (not (eq
|
||||
$re .File.BaseFileName)) }} {{ $backlinks = $backlinks | append . }} {{ end }}
|
||||
{{ end }} {{ if gt (len $backlinks) 0 }}
|
||||
<aside>
|
||||
<h4>Backlinks</h4>
|
||||
<div class="backlinks">
|
||||
<ul>
|
||||
{{ range $backlinks }}
|
||||
<li class="capitalize"><a href="{{ .RelPermalink }}">{{ .Title }}</a></li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
{{ else }}
|
||||
<aside>
|
||||
<h4>No notes link to this note</h4>
|
||||
</aside>
|
||||
{{ end }}
|
||||
|
||||
<aside class="related">
|
||||
{{ $related := .Site.RegularPages.Related . | complement $backlinks | first 3
|
||||
-}} {{ with $related -}}
|
||||
<h4>slightly related</h4>
|
||||
<ul>
|
||||
{{ range . -}}
|
||||
<li class="capitalize"><a href="{{ .RelPermalink }}">{{ .Title }}</a></li>
|
||||
{{ end -}}
|
||||
</ul>
|
||||
{{ end -}}
|
||||
</aside>
|
1
layouts/partials/bottom_home.html
Normal file
1
layouts/partials/bottom_home.html
Normal file
|
@ -0,0 +1 @@
|
|||
{{ partial "bottom_list.html" . }}
|
5
layouts/partials/bottom_list.html
Normal file
5
layouts/partials/bottom_list.html
Normal file
|
@ -0,0 +1,5 @@
|
|||
</main>
|
||||
{{ partial "footer.html" . }}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
1
layouts/partials/bottom_single.html
Normal file
1
layouts/partials/bottom_single.html
Normal file
|
@ -0,0 +1 @@
|
|||
{{ partial "bottom_list.html" . }}
|
24
layouts/partials/comments.html
Normal file
24
layouts/partials/comments.html
Normal file
|
@ -0,0 +1,24 @@
|
|||
<script defer src="https://cdn.commento.io/js/commento.js" data-no-fonts="true"></script>
|
||||
<div id="commento"></div>
|
||||
|
||||
<div class="comments webmentions">
|
||||
<iframe id="fediverse" src="/api/activitypub-html/render.ts?url={{.Page.Permalink}}">
|
||||
</iframe>
|
||||
<script>
|
||||
// Selecting the iframe element
|
||||
const fediverseFrame = document.getElementById("fediverse");
|
||||
fediverseFrame.onload = function () {
|
||||
fediverseFrame.style.height = `${fediverseFrame.contentWindow.document.body.scrollHeight}px`;
|
||||
}
|
||||
</script>
|
||||
<!--<iframe id="mentions" src="/api/mentions.js?url={{.Page.Permalink}}"></iframe>-->
|
||||
<script>
|
||||
// Selecting the iframe element
|
||||
const mentionsFrame = document.getElementById("mentions");
|
||||
if (mentionsFrame) {
|
||||
mentionsFrame.onload = function () {
|
||||
mentionsFrame.style.height = `${mentionsFrame.contentWindow.document.body.scrollHeight}px`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</div>
|
11
layouts/partials/draft.html
Normal file
11
layouts/partials/draft.html
Normal file
|
@ -0,0 +1,11 @@
|
|||
<div class="draft">
|
||||
<p><strong>Disclaimer</strong>. This article is a <strong>draft</strong>.</p>
|
||||
<p>This article might look complete but it is completely un-developed and is a snapshot of my thoughts at
|
||||
the time (which might also be a long time ago). It will be refined over time with research, education and
|
||||
conversations with people in the industry.</p>
|
||||
<p>There are many errors including and not limited to: spelling, grammatical, lack of ecosystem awareness
|
||||
understanding and plain ignorance.</p>
|
||||
<p>I am making it public to get feedback. You might completely disagree, you might agree violently. Either
|
||||
way, teach me. I want to hear from you: (<a href="mailto:flear@flear.org">Email</a>, <a
|
||||
href="https://twitter.com/@FLEAR_radio/">@FLEAR_radio</a>.)</p>
|
||||
</div>
|
17
layouts/partials/footer.html
Normal file
17
layouts/partials/footer.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
<footer class='site-footer clearfix container text-muted'>
|
||||
<hr>
|
||||
<div class='row'>
|
||||
<div class='col-md-6'>
|
||||
<a data-scroll='true' href='#top'>
|
||||
<i class='fa fa-arrow-up'></i> Back to top </a>
|
||||
</div>
|
||||
<div class='col-md-6 text-right'>
|
||||
<p>
|
||||
<strong>FLEAR</strong> © 2023 to present
|
||||
</p>
|
||||
<p>
|
||||
<a href='https://git.qoto.org/flear/flear-side'>Get source</a> by <a href='https://jeffreyfreeman.me'>Jeffrey Phillips Freeman</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
119
layouts/partials/head.html
Normal file
119
layouts/partials/head.html
Normal file
|
@ -0,0 +1,119 @@
|
|||
<!DOCTYPE html>
|
||||
<!--[if IEMobile 7 ]><html class="iem7"><![endif]-->
|
||||
<html lang="{{.Page.Lang}}">
|
||||
<head lang="{{.Page.Lang}}">
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link rel="preconnect" href="https://www.google-analytics.com" crossorigin>
|
||||
{{ range $.Page.AllTranslations }}
|
||||
<link rel="alternate" hreflang="{{.Lang}}" href="{{.Permalink}}" />{{if eq .Lang "en"}}
|
||||
<link rel="alternate" hreflang="x-default" href="{{.Permalink}}" />{{end}}
|
||||
{{ end }}
|
||||
<title>{{if .IsPage }}{{ if .Title }}{{ .Title }}{{ end }}{{else}}{{.Site.Title}}{{end}}</title>
|
||||
<link rel="canonical" href="{{ .Page.Permalink }}" />
|
||||
<meta name="author" content="{{ .Site.Params.author }}">
|
||||
<link rel="webmention" href="https://webmention.io/flear.org/webmention" />
|
||||
<link rel="pingback" href="https://webmention.io/flear.org/xmlrpc" />
|
||||
<meta name="supported-color-schemes" content="light dark">
|
||||
|
||||
<meta name="theme-color" content="{{ if .Params.themecolor }}#{{ .Params.themecolor}}{{ else }}#000000{{ end }}">
|
||||
<meta property="og:type" content="website" />
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
{{if .Params.image_header}}
|
||||
<meta name="twitter:image" content="{{.Params.image_header}}">
|
||||
{{else}}
|
||||
{{ $tags := .Params.tags}}
|
||||
{{ if .Params.social_image_url }}
|
||||
{{ $socialURL := printf "https://flear.org/api/card?%s" (querify "title" .Title "imgUrl" .Params.social_image_url ) }}
|
||||
<meta name="twitter:image" {{ printf "content=%q" $socialURL | safeHTMLAttr}}>
|
||||
<meta property="og:image" {{ printf "content=%q" $socialURL | safeHTMLAttr}}>
|
||||
{{else}}
|
||||
{{ $socialURL := printf "https://flear.org/api/card?%s" (querify "title" .Title) }}
|
||||
<meta name="twitter:image" {{ printf "content=%q" $socialURL | safeHTMLAttr}}>
|
||||
<meta property="og:image" {{ printf "content=%q" $socialURL | safeHTMLAttr}}>
|
||||
{{end}}
|
||||
{{end}}
|
||||
<meta name="twitter:site" content="@FLEAR_radio">
|
||||
<meta name="twitter:creator" content="@FLEAR_radio">
|
||||
{{ if .Title }}
|
||||
<meta name="twitter:title" content="{{ .Title }} ">
|
||||
{{end}}
|
||||
<meta name="description" content="{{if .Description}}{{ .Description }}{{else}}{{.Site.Params.Description}}{{end}}">
|
||||
<meta name="twitter:description" content="{{if .Description}}{{ .Description }}{{else}}{{.Site.Params.Description}}{{end}}">
|
||||
<script defer src="/_vercel/insights/script.js"></script>
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "http://schema.org",
|
||||
"@type": "Person",
|
||||
"name": "FLEAR",
|
||||
"url": "https://flear.org/",
|
||||
"sameAs": [
|
||||
"https://twitter.com/FLEAR_radio",
|
||||
"https://git.qoto.org/flear",
|
||||
]
|
||||
}
|
||||
</script>
|
||||
{{ if eq .Type "page" }}
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "http://schema.org",
|
||||
"@type": "NewsArticle",
|
||||
"headline": {{.Title }},
|
||||
"datePublished": "{{ .Date.Format "2006-01-02T15:04:05" }}",
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "FLEAR",
|
||||
"url": "https://flear.org/"
|
||||
},
|
||||
"description": "{{if .Description}}{{ .Description }}{{else}}{{.Summary}}{{end}}"
|
||||
}
|
||||
</script>
|
||||
{{ end }}
|
||||
<link href="https://flear.org/flear" rel="alternate" type="application/activity+json">
|
||||
<link rel="polymath" href="/polymath">
|
||||
<link href="/images/logo.png" rel="icon">
|
||||
<link href="/manifest.json" rel="manifest">
|
||||
<link rel="home" href="{{.Site.BaseURL}}">
|
||||
<style>
|
||||
{{ $main := resources.Get "css/main.css" }}
|
||||
{{ $goblin := resources.Get "css/goblin.css" }}
|
||||
{{ $fa := resources.Get "css/fontawesome.css" }}
|
||||
{{ $primer := resources.Get "css/primer.css" }}
|
||||
{{ $ghost := resources.Get "css/ghost.css" }}
|
||||
{{ $glyph := resources.Get "css/glyphicons.css" }}
|
||||
|
||||
{{ $css := slice $main $goblin $fa $primer $ghost $glyph | resources.Concat "css/bundle.css" }}
|
||||
{{ $minifiedCSS := $css | minify}}
|
||||
{{ $minifiedCSS.Content | safeCSS }}
|
||||
|
||||
header.with-background {
|
||||
{{ if .Params.image_header }}background-image: url('{{ .Params.image_header}}');{{ end }}
|
||||
}
|
||||
</style>
|
||||
{{ range .AlternativeOutputFormats -}}
|
||||
<link rel="{{ .Rel }}" type="{{ .MediaType.Type }}" href="{{ .Permalink | safeURL }}">
|
||||
{{ end -}}
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-V4DZ9TE0NV"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-V4DZ9TE0NV');
|
||||
</script>
|
||||
|
||||
<script>
|
||||
{{ $jqueryJS := resources.Get "javascripts/jquery.js" | minify }}
|
||||
{{ $jqueryJS.Content | safeJS }}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
let type='{{.Kind}}';
|
||||
{{ $headJS := resources.Get "javascripts/head.js" | minify }}
|
||||
{{ $headJS.Content | safeJS }}
|
||||
</script>
|
||||
</head>
|
52
layouts/partials/menu.html
Normal file
52
layouts/partials/menu.html
Normal file
|
@ -0,0 +1,52 @@
|
|||
<nav class="navbar navbar--light navbar--fixed-top">
|
||||
<div class="navbar__inner">
|
||||
<div class="navbar__items navbar__items--left navbarItems_1xGM">
|
||||
<a aria-current="page" class="navbar__brand active" href="/">
|
||||
<img class="navbar__logo logo--light" src="/images/logo-header-light.png" alt="FLEAR">
|
||||
<img class="navbar__logo logo--dark" src="/images/logo-header-dark.png" alt="FLEAR">
|
||||
</a>
|
||||
<div aria-label="Navigation bar toggle" class="navbar__toggle" role="button" tabindex="0">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 30 30" role="img" focusable="false">
|
||||
<title>Menu</title>
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2" d="M4 7h22M4 15h22M4 23h22"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar__items navbar__items--right">
|
||||
<a class="navbar__item navbar__link" href="/">Home</a>
|
||||
<a class="navbar__item navbar__link" href="/news">News</a>
|
||||
<a class="navbar__item navbar__link" href="/projects">Projects</a>
|
||||
<a class="navbar__item navbar__link" href="/resource">Resources</a>
|
||||
<a class="displayOnlyInLargeViewport_16CL button button--primary" href="https://git.qoto.org/flear" style="margin:0;margin-left:15px">FLEAR GITLAB →</a>
|
||||
</div>
|
||||
</div>
|
||||
<div role="presentation" class="navbar-sidebar__backdrop"></div>
|
||||
<div class="navbar-sidebar">
|
||||
<div class="navbar-sidebar__brand">
|
||||
<a aria-current="page" class="navbar__brand active" href="/">
|
||||
<img class="navbar__logo logo--light" src="/images/logo-header-light.png" alt="Goblin">
|
||||
<img class="navbar__logo logo--dark" src="/images/logo-header-dark.png" alt="Goblin">
|
||||
</a>
|
||||
</div>
|
||||
<div class="navbar-sidebar__items">
|
||||
<div class="menu">
|
||||
<ul class="menu__list">
|
||||
<li class="menu__list-item">
|
||||
<a class="menu__link" href="/">Home</a>
|
||||
</li>
|
||||
<li class="menu__list-item">
|
||||
<a class="menu__link" href="/news">News</a>
|
||||
</li>
|
||||
<li class="menu__list-item">
|
||||
<a class="menu__link" href="/projects">Projects</a>
|
||||
</li>
|
||||
<li class="menu__list-item">
|
||||
<a class="menu__link" href="/resource">Resources</a>
|
||||
</li>
|
||||
<div style="margin:5px;margin-top:15px"></div>
|
||||
<a class="button button--block button--primary" href="https://git.qoto.org/flear">OUR GITLAB →</a>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
8
layouts/partials/microblog.html
Normal file
8
layouts/partials/microblog.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
<div class='menu microblog'>
|
||||
<span class='menu-heading'> Microblog </span>
|
||||
<iframe allowfullscreen sandbox="allow-top-navigation allow-scripts allow-popups allow-popups-to-escape-sandbox" style='border-width: 0' width='100%' height="800" src="https://www.mastofeed.com/apiv2/feed?userurl=https%3A%2F%2Fqoto.org%2Fusers%2FFLEAR&theme=light&size=80&header=false&replies=false&boosts=false"></iframe>
|
||||
<div class="fill-vert"></div>
|
||||
<div class="more">
|
||||
<a class='btn btn-default btn-m pull-right' href='https://qoto.org/@FLEAR/' style='align:right;text-align: right;align-content: right;'>More ... </a>
|
||||
</div>
|
||||
</div>
|
6
layouts/partials/top_home.html
Normal file
6
layouts/partials/top_home.html
Normal file
|
@ -0,0 +1,6 @@
|
|||
{{ partial "head.html" . }}
|
||||
<body class="list h-feed">
|
||||
<div>
|
||||
{{ partial "menu.html" . }}
|
||||
<div class="main-wrapper">
|
||||
<main class="hfeed">
|
12
layouts/partials/top_list_generic.html
Normal file
12
layouts/partials/top_list_generic.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
{{ partial "head.html" . }}
|
||||
<body class="list h-feed">
|
||||
<div>
|
||||
{{ partial "menu.html" . }}
|
||||
<div class="main-wrapper">
|
||||
<header class='subheader'>
|
||||
<div class='container'>
|
||||
<h2 class='page-title'> FLEAR <small>Free & Libre Engineers for Amateur Radio</small>
|
||||
</h2>
|
||||
</div>
|
||||
</header>
|
||||
<main class="hfeed container" id="content" role="main">
|
12
layouts/partials/top_list_news.html
Normal file
12
layouts/partials/top_list_news.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
{{ partial "head.html" . }}
|
||||
<body class="list h-feed">
|
||||
<div>
|
||||
{{ partial "menu.html" . }}
|
||||
<div class="main-wrapper">
|
||||
<header class='subheader'>
|
||||
<div class='container'>
|
||||
<h2 class='page-title'> News <small>For current events in FLEAR and its projects. You can follow this page on the fediverse at @flear@flear.org.</small>
|
||||
</h2>
|
||||
</div>
|
||||
</header>
|
||||
<main class="hfeed container" id="content" role="main">
|
12
layouts/partials/top_list_projects.html
Normal file
12
layouts/partials/top_list_projects.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
{{ partial "head.html" . }}
|
||||
<body class="list h-feed">
|
||||
<div>
|
||||
{{ partial "menu.html" . }}
|
||||
<div class="main-wrapper">
|
||||
<header class='subheader'>
|
||||
<div class='container'>
|
||||
<h2 class='page-title'> Projects <small>Open source projects past of our incubator for Ham Radio.</small>
|
||||
</h2>
|
||||
</div>
|
||||
</header>
|
||||
<main class="hfeed container" id="content" role="main">
|
12
layouts/partials/top_list_resource.html
Normal file
12
layouts/partials/top_list_resource.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
{{ partial "head.html" . }}
|
||||
<body class="list h-feed">
|
||||
<div>
|
||||
{{ partial "menu.html" . }}
|
||||
<div class="main-wrapper">
|
||||
<header class='subheader'>
|
||||
<div class='container'>
|
||||
<h2 class='page-title'> Resources <small>services, assistance, information, and other resources to assist the FLOSS Ham.</small>
|
||||
</h2>
|
||||
</div>
|
||||
</header>
|
||||
<main class="hfeed container" id="content" role="main">
|
17
layouts/partials/top_single.html
Normal file
17
layouts/partials/top_single.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
{{ partial "head.html" . }}
|
||||
<body class="single">
|
||||
<div>
|
||||
{{ partial "menu.html" . }}
|
||||
<div class="main-wrapper">
|
||||
<header class='subheader'>
|
||||
<div class='container'>
|
||||
<h2 class='page-title'>
|
||||
<small>
|
||||
<a href='javascript:history.back()'>
|
||||
<i class='fa fa-chevron-left'></i>
|
||||
</a>
|
||||
</small> {{ if .Title }}{{ .Title }}{{else}}{{.Site.Title}}{{end}}
|
||||
</h2>
|
||||
</div>
|
||||
</header>
|
||||
<main class="hfeed container" id="content" role="main">
|
91
layouts/projects/list.html
Normal file
91
layouts/projects/list.html
Normal file
|
@ -0,0 +1,91 @@
|
|||
{{ partial "top_list_projects.html" . }}
|
||||
|
||||
{{ $paginator := .Paginate (where .Data.Pages ".Section" "projects") }}
|
||||
<div class='row'>
|
||||
<div class='col-md-8'>
|
||||
<div class='extra-pagination inner'>
|
||||
<nav class='pagination' role='pagination'>
|
||||
{{ if .Paginator.HasNext }}<a href="{{ .Paginator.Next.URL }}" class="newer-posts">← Newer Posts</a>{{ end }}
|
||||
<span class='page-number'>Page {{ .Paginator.PageNumber }} of {{ .Paginator.TotalPages }}</span>
|
||||
{{ if .Paginator.HasPrev }}<a href="{{ .Paginator.Prev.URL }}" class="older-posts">Older Posts →</a>{{ end }}
|
||||
</nav>
|
||||
</div>
|
||||
{{ range $index, $element := $paginator.Pages }}
|
||||
|
||||
<article class='post'>
|
||||
<header class='post-header'>
|
||||
<h2 class='post-title'>
|
||||
<a href="{{ .Permalink }}">{{ .Title}}</a>
|
||||
{{ if .Params.link }} — <a href="{{.Params.link}}">🔗</a>{{end}}
|
||||
</h2>
|
||||
<section class='post-meta text-muted'>
|
||||
<ul class='list-inline'>
|
||||
<li>
|
||||
<i class='fa fa-calendar'></i>
|
||||
<time class='post-date' datetime='{{ .Date.Format "2020-09-10" }}'> {{ .Date.Format "January 2 2006" }} </time>
|
||||
</li>
|
||||
<li>
|
||||
<a class='fa-solid fa-book-open' href='{{ .Permalink }}'>
|
||||
<span class='hidden'>Book icon</span>
|
||||
</a>
|
||||
<a href='{{ .Permalink }}'>Reading time: {{ .ReadingTime }} minute{{ if (ne .ReadingTime 1) }}s{{ end }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<i class='fa fa-folder-open'></i>
|
||||
{{ $tags := .Params.tags}}
|
||||
{{ with $tags }}
|
||||
{{ range . }}
|
||||
<a href="{{ "/tags/" | relLangURL }}{{ . | urlize }}">{{ . }}</a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</header>
|
||||
<section class='post-content'>
|
||||
{{.Content }}
|
||||
</section>
|
||||
</article>
|
||||
|
||||
{{ end }}
|
||||
|
||||
<div class='extra-pagination inner'>
|
||||
<nav class='pagination' role='pagination'>
|
||||
{{ if .Paginator.HasNext }}<a href="{{ .Paginator.Next.URL }}" class="newer-posts">← Newer Posts</a>{{ end }}
|
||||
<span class='page-number'>Page {{ .Paginator.PageNumber }} of {{ .Paginator.TotalPages }}</span>
|
||||
{{ if .Paginator.HasPrev }}<a href="{{ .Paginator.Prev.URL }}" class="older-posts">Older Posts →</a>{{ end }}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class='col-md-4'>
|
||||
<nav class='menu tags'>
|
||||
<span class="menu-heading">Project Categories</span>
|
||||
<a class='menu-item selected' data-slug='all' href='/projects'>All posts</a>
|
||||
{{ $hereSection := .Section }}
|
||||
{{/* Get current taxonomy (example: tags, categories). */}}
|
||||
<!-- {{ $taxonomy := .Data.Plural }} -->
|
||||
{{ $taxonomy := "tags" }}
|
||||
{{/* Get sections where one or more pages has one or more terms in the current taxonomy. */}}
|
||||
{{ $sections := slice }}
|
||||
{{ range $term, $weightedPages := index site.Taxonomies $taxonomy }}
|
||||
{{ range $weightedPages }}
|
||||
{{ $sections = $sections | append .CurrentSection | uniq | sort }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{/* List pages by term by section. */}}
|
||||
{{ range $section := (where $sections ".Path" $hereSection) }}
|
||||
{{ range $term, $weightedPages := index site.Taxonomies $taxonomy }}
|
||||
{{ $termPage := site.GetPage path.Join $taxonomy $term }}
|
||||
{{ with where $weightedPages "Section" $section.Path }}
|
||||
<a href="{{ $termPage.RelPermalink }}" class='menu-item tag' data-slug='{{ $termPage.LinkTitle }}' title="All pages with tag <i>{{ $termPage.LinkTitle }}</i>">{{ $termPage.LinkTitle }}</a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</nav>
|
||||
<br>
|
||||
{{ partial "microblog.html" . }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ partial "bottom_list.html" . }}
|
67
layouts/projects/single.html
Normal file
67
layouts/projects/single.html
Normal file
|
@ -0,0 +1,67 @@
|
|||
{{ partial "top_single.html" . }}
|
||||
<div class='row'>
|
||||
<div class='col-md-8'>
|
||||
<article class='post'>
|
||||
<header class='post-header'>
|
||||
<section class='post-meta text-muted'>
|
||||
<ul class='list-inline'>
|
||||
<li>
|
||||
<i class='fa fa-calendar'></i>
|
||||
<time class='post-date' datetime='{{ .Date.Format "2020-09-10" }}'> {{ .Date.Format "January 2 2006" }} </time>
|
||||
</li>
|
||||
<li>
|
||||
<a class='fa-solid fa-book-open' href='{{ .Permalink }}'>
|
||||
<span class='hidden'>Book icon</span>
|
||||
</a>
|
||||
<a href='{{ .Permalink }}'>Reading time: {{ .ReadingTime }} minute{{ if (ne .ReadingTime 1) }}s{{ end }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<i class='fa fa-folder-open'></i>
|
||||
{{ $tags := .Params.tags}}
|
||||
{{ with $tags }}
|
||||
{{ range . }}
|
||||
<a href="{{ "/tags/" | relLangURL }}{{ . | urlize }}">{{ . }}</a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</header>
|
||||
<section class='post-content'>
|
||||
{{ .Content }}
|
||||
|
||||
{{ partial "article_footer.html" . }}
|
||||
<section class='share text-center'>
|
||||
<a class='share-twitter' href='https://twitter.com/intent/tweet?text={{ if .Title }}{{ .Title }}{{else}}{{.Site.Title}}{{end}}amp;url={{ .Permalink }}' onclick='window.open(this.href, 'twitter-share', 'width=550,height=235');return false;' true=''>
|
||||
<span class='fa-stack fa-lg'>
|
||||
<i class='fa fa-circle fa-stack-2x'></i>
|
||||
<i class='fa-brands fa-twitter fa-stack-1x fa-inverse'></i>
|
||||
</span>
|
||||
</a>
|
||||
<a class='share-facebook' href='https://www.facebook.com/sharer/sharer.php?u={{ .Permalink }}' onclick='window.open(this.href, 'facebook-share','width=580,height=296');return false;' true=''>
|
||||
<span class='fa-stack fa-lg'>
|
||||
<i class='fa fa-circle fa-stack-2x'></i>
|
||||
<i class='fa-brands fa-facebook-f fa-stack-1x fa-inverse'></i>
|
||||
</span>
|
||||
</a>
|
||||
</section>
|
||||
{{ partial "comments.html" . }}
|
||||
</section>
|
||||
</article>
|
||||
</div>
|
||||
<div class='col-md-4'>
|
||||
<nav class='menu tags'>
|
||||
<span class="menu-heading">Recent Project Posts</span>
|
||||
<a class='menu-item' data-slug='all' href='/projects'>All posts</a>
|
||||
{{ $hereLink := .Permalink }}
|
||||
{{ range (where .Site.Pages "Section" "projects") }}
|
||||
{{ range .Pages }}
|
||||
<a href='{{ .RelPermalink }}' class='menu-item tag{{ if (eq .Permalink $hereLink) }} selected{{ end }}' data-slug='{{ .Title}}' title="{{ .Title}}">{{ .Title}}</a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</nav>
|
||||
<br>
|
||||
{{ partial "microblog.html" . }}
|
||||
</div>
|
||||
</div>
|
||||
{{ partial "bottom_single.html" . }}
|
91
layouts/resource/list.html
Normal file
91
layouts/resource/list.html
Normal file
|
@ -0,0 +1,91 @@
|
|||
{{ partial "top_list_resource.html" . }}
|
||||
|
||||
{{ $paginator := .Paginate (where .Data.Pages ".Section" "resource") }}
|
||||
<div class='row'>
|
||||
<div class='col-md-8'>
|
||||
<div class='extra-pagination inner'>
|
||||
<nav class='pagination' role='pagination'>
|
||||
{{ if .Paginator.HasNext }}<a href="{{ .Paginator.Next.URL }}" class="newer-posts">← Newer Posts</a>{{ end }}
|
||||
<span class='page-number'>Page {{ .Paginator.PageNumber }} of {{ .Paginator.TotalPages }}</span>
|
||||
{{ if .Paginator.HasPrev }}<a href="{{ .Paginator.Prev.URL }}" class="older-posts">Older Posts →</a>{{ end }}
|
||||
</nav>
|
||||
</div>
|
||||
{{ range $index, $element := $paginator.Pages }}
|
||||
|
||||
<article class='post'>
|
||||
<header class='post-header'>
|
||||
<h2 class='post-title'>
|
||||
<a href="{{ .Permalink }}">{{ .Title}}</a>
|
||||
{{ if .Params.link }} — <a href="{{.Params.link}}">🔗</a>{{end}}
|
||||
</h2>
|
||||
<section class='post-meta text-muted'>
|
||||
<ul class='list-inline'>
|
||||
<li>
|
||||
<i class='fa fa-calendar'></i>
|
||||
<time class='post-date' datetime='{{ .Date.Format "2020-09-10" }}'> {{ .Date.Format "January 2 2006" }} </time>
|
||||
</li>
|
||||
<li>
|
||||
<a class='fa-solid fa-book-open' href='{{ .Permalink }}'>
|
||||
<span class='hidden'>Book icon</span>
|
||||
</a>
|
||||
<a href='{{ .Permalink }}'>Reading time: {{ .ReadingTime }} minute{{ if (ne .ReadingTime 1) }}s{{ end }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<i class='fa fa-folder-open'></i>
|
||||
{{ $tags := .Params.tags}}
|
||||
{{ with $tags }}
|
||||
{{ range . }}
|
||||
<a href="{{ "/tags/" | relLangURL }}{{ . | urlize }}">{{ . }}</a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</header>
|
||||
<section class='post-content'>
|
||||
{{.Content }}
|
||||
</section>
|
||||
</article>
|
||||
|
||||
{{ end }}
|
||||
|
||||
<div class='extra-pagination inner'>
|
||||
<nav class='pagination' role='pagination'>
|
||||
{{ if .Paginator.HasNext }}<a href="{{ .Paginator.Next.URL }}" class="newer-posts">← Newer Posts</a>{{ end }}
|
||||
<span class='page-number'>Page {{ .Paginator.PageNumber }} of {{ .Paginator.TotalPages }}</span>
|
||||
{{ if .Paginator.HasPrev }}<a href="{{ .Paginator.Prev.URL }}" class="older-posts">Older Posts →</a>{{ end }}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class='col-md-4'>
|
||||
<nav class='menu tags'>
|
||||
<span class="menu-heading">Resource Categories</span>
|
||||
<a class='menu-item selected' data-slug='all' href='/resource'>All posts</a>
|
||||
{{ $hereSection := .Section }}
|
||||
{{/* Get current taxonomy (example: tags, categories). */}}
|
||||
<!-- {{ $taxonomy := .Data.Plural }} -->
|
||||
{{ $taxonomy := "tags" }}
|
||||
{{/* Get sections where one or more pages has one or more terms in the current taxonomy. */}}
|
||||
{{ $sections := slice }}
|
||||
{{ range $term, $weightedPages := index site.Taxonomies $taxonomy }}
|
||||
{{ range $weightedPages }}
|
||||
{{ $sections = $sections | append .CurrentSection | uniq | sort }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{/* List pages by term by section. */}}
|
||||
{{ range $section := (where $sections ".Path" $hereSection) }}
|
||||
{{ range $term, $weightedPages := index site.Taxonomies $taxonomy }}
|
||||
{{ $termPage := site.GetPage path.Join $taxonomy $term }}
|
||||
{{ with where $weightedPages "Section" $section.Path }}
|
||||
<a href="{{ $termPage.RelPermalink }}" class='menu-item tag' data-slug='{{ $termPage.LinkTitle }}' title="All pages with tag <i>{{ $termPage.LinkTitle }}</i>">{{ $termPage.LinkTitle }}</a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</nav>
|
||||
<br>
|
||||
{{ partial "microblog.html" . }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ partial "bottom_list.html" . }}
|
68
layouts/resource/single.html
Normal file
68
layouts/resource/single.html
Normal file
|
@ -0,0 +1,68 @@
|
|||
{{ partial "top_single.html" . }}
|
||||
<div class='row'>
|
||||
<div class='col-md-8'>
|
||||
<article class='post'>
|
||||
<header class='post-header'>
|
||||
<section class='post-meta text-muted'>
|
||||
<ul class='list-inline'>
|
||||
<li>
|
||||
<i class='fa fa-calendar'></i>
|
||||
<time class='post-date' datetime='{{ .Date.Format "2020-09-10" }}'> {{ .Date.Format "January 2 2006" }} </time>
|
||||
</li>
|
||||
<li>
|
||||
<a class='fa-solid fa-book-open' href='{{ .Permalink }}'>
|
||||
<span class='hidden'>Book icon</span>
|
||||
</a>
|
||||
<a href='{{ .Permalink }}'>Reading time: {{ .ReadingTime }} minute{{ if (ne .ReadingTime 1) }}s{{ end }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<i class='fa fa-folder-open'></i>
|
||||
{{ $tags := .Params.tags}}
|
||||
{{ with $tags }}
|
||||
{{ range . }}
|
||||
<a href="{{ "/tags/" | relLangURL }}{{ . | urlize }}">{{ . }}</a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</header>
|
||||
<section class='post-content'>
|
||||
{{ if isset .Params "toc" }}{{ .TableOfContents }}{{ end}}
|
||||
{{ .Content }}
|
||||
{{ partial "backlinks.html" . }}
|
||||
{{ partial "article_footer.html" . }}
|
||||
<section class='share text-center'>
|
||||
<a class='share-twitter' href='https://twitter.com/intent/tweet?text={{ if .Title }}{{ .Title }}{{else}}{{.Site.Title}}{{end}}amp;url={{ .Permalink }}' onclick='window.open(this.href, 'twitter-share', 'width=550,height=235');return false;' true=''>
|
||||
<span class='fa-stack fa-lg'>
|
||||
<i class='fa fa-circle fa-stack-2x'></i>
|
||||
<i class='fa-brands fa-twitter fa-stack-1x fa-inverse'></i>
|
||||
</span>
|
||||
</a>
|
||||
<a class='share-facebook' href='https://www.facebook.com/sharer/sharer.php?u={{ .Permalink }}' onclick='window.open(this.href, 'facebook-share','width=580,height=296');return false;' true=''>
|
||||
<span class='fa-stack fa-lg'>
|
||||
<i class='fa fa-circle fa-stack-2x'></i>
|
||||
<i class='fa-brands fa-facebook-f fa-stack-1x fa-inverse'></i>
|
||||
</span>
|
||||
</a>
|
||||
</section>
|
||||
{{ partial "comments.html" . }}
|
||||
</section>
|
||||
</article>
|
||||
</div>
|
||||
<div class='col-md-4'>
|
||||
<nav class='menu tags'>
|
||||
<span class="menu-heading">Recent Resource Posts</span>
|
||||
<a class='menu-item' data-slug='all' href='/resource'>All posts</a>
|
||||
{{ $hereLink := .Permalink }}
|
||||
{{ range (where .Site.Pages "Section" "resource") }}
|
||||
{{ range .Pages }}
|
||||
<a href='{{ .RelPermalink }}' class='menu-item tag{{ if (eq .Permalink $hereLink) }} selected{{ end }}' data-slug='{{ .Title}}' title="{{ .Title}}">{{ .Title}}</a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</nav>
|
||||
<br>
|
||||
{{ partial "microblog.html" . }}
|
||||
</div>
|
||||
</div>
|
||||
{{ partial "bottom_single.html" . }}
|
4
layouts/shortcodes/card-row.html
Normal file
4
layouts/shortcodes/card-row.html
Normal file
|
@ -0,0 +1,4 @@
|
|||
|
||||
<div class="row">
|
||||
{{ .Inner }}
|
||||
</div>
|
21
layouts/shortcodes/card.html
Normal file
21
layouts/shortcodes/card.html
Normal file
|
@ -0,0 +1,21 @@
|
|||
<!--
|
||||
0: Title
|
||||
1: Link out
|
||||
2: left bottom text
|
||||
3: right bottom text
|
||||
inner: description
|
||||
-->
|
||||
|
||||
<div class="col">
|
||||
<a class="card" href="{{ .Get 1 }}">
|
||||
<div class="card__body">
|
||||
<h3 style="margin:0;text-transform:capitalize">{{ .Get 0 }}</h3>
|
||||
<small>{{ .Inner }}</small>
|
||||
</div>
|
||||
<hr style="margin:15px 0 10px 0">
|
||||
<div style="display:flex;flex-direction:row;justify-content:space-between;padding:0 15px 10px 15px;font-size:0.8em">
|
||||
<div>{{ .Get 2 }}</div>
|
||||
<div>{{ .Get 3 }}</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
16
layouts/shortcodes/cards.html
Normal file
16
layouts/shortcodes/cards.html
Normal file
|
@ -0,0 +1,16 @@
|
|||
<!--
|
||||
0: Tttle
|
||||
1: Description
|
||||
inner: card-row
|
||||
-->
|
||||
<section class="section-lg">
|
||||
<div class="container">
|
||||
<div class="row responsiveCentered_2A7S">
|
||||
<div class="col col--6 col--offset-3">
|
||||
<h2 class="with-underline">{{ .Get 0 }}</h2>
|
||||
<p>{{ .Get 1 }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{ .Inner }}
|
||||
</div>
|
||||
</section>
|
8
layouts/shortcodes/drafts.html
Normal file
8
layouts/shortcodes/drafts.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
<ul>
|
||||
{{ $pages := .Site.RegularPages }}
|
||||
{{ range (where $pages ".Draft" "!=" false) }}
|
||||
<li><strong>{{dateFormat "Jan 2, 2006" .PublishDate}}</strong> - <a href="{{.Permalink}}">{{.Title}}</a> <br>
|
||||
<blockquote>{{.Summary}}</blockquote>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
9
layouts/shortcodes/example.html
Normal file
9
layouts/shortcodes/example.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
<div class="code-with-header">
|
||||
<div class="code-header">{{ .Get 0 }}</div>
|
||||
<div class="scrollbar-container">
|
||||
<div class="codeBlockWrapper_2QGZ">
|
||||
{{ .Inner }}
|
||||
<button type="button" aria-label="Copy code to clipboard" class="copyButton_1BYj">Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
7
layouts/shortcodes/fast-youtube.html
Normal file
7
layouts/shortcodes/fast-youtube.html
Normal file
|
@ -0,0 +1,7 @@
|
|||
<div class="yt-embed" style="position:relative;padding-bottom: 56.25%; padding-top: 25px; height: 0;">
|
||||
<iframe
|
||||
data-src="//www.youtube-nocookie.com/embed/{{ index .Params 0 }}?autoplay=1"
|
||||
style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; border:0;"
|
||||
allowfullscreen
|
||||
title="YouTube Video"></iframe>
|
||||
</div>
|
13
layouts/shortcodes/figure.html
Normal file
13
layouts/shortcodes/figure.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<figure>
|
||||
<img
|
||||
{{ with .Get "src" }}src="{{ . }}"{{ end }}
|
||||
{{ with .Get "alt" }}alt="{{ range (split . " ") }}{{ . }} {{ end }}"{{ end }}
|
||||
{{ with .Get "attribution" }}attribution="{{ range (split . " ") }}{{ . }} {{ end }}"{{ end }}
|
||||
{{ with .Get "srcset" }}srcset="{{ range (split . " ") }}{{ . }} {{ end }}"{{ end }}
|
||||
{{ with .Get "tabindex" }}tabindex="{{ . }}"{{ end }}
|
||||
{{ with .Get "role" }}role="{{ . }}"{{ end }}
|
||||
{{ with .Get "width" }}width="{{ . }}"{{ end }}
|
||||
{{ with .Get "height" }}height="{{ . }}"{{ end }}
|
||||
{{ with .Get "layout" }}layout="{{ . }}"{{ end }} />
|
||||
{{ with .Get "caption" }}<figcaption>{{ . }}</figcaption>{{ end }}
|
||||
</figure>
|
1
layouts/shortcodes/html.html
Normal file
1
layouts/shortcodes/html.html
Normal file
|
@ -0,0 +1 @@
|
|||
{{.Inner}}
|
8
layouts/shortcodes/info-buttons.html
Normal file
8
layouts/shortcodes/info-buttons.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
<section style="margin-top:100px;padding:50px 0;border-top:1px solid var(--custom-border-color)" class="hero is--dark">
|
||||
<div class="container text--center">
|
||||
<div>
|
||||
<a class="button hero--button button--outline button--md button--secondary responsive-button button_2W20" style="margin:5px" href="{{ .Get 1 }}">{{ .Get 0 }}</a>
|
||||
<a class="button hero--button button--md button--primary responsive-button button_2W20" to="https://git.qoto.org/goblin-ogm/goblin" style="margin:5px" href="{{ .Get 3 }}">{{ .Get 2 }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
56
layouts/shortcodes/list.html
Normal file
56
layouts/shortcodes/list.html
Normal file
|
@ -0,0 +1,56 @@
|
|||
{{ partial "head.html" . }}
|
||||
|
||||
<body class="list">
|
||||
<header class="me">
|
||||
<div>
|
||||
<img src="/images/logo.png" alt="A dashing picture of FLEAR.">
|
||||
<h1>Hello.</h1>
|
||||
<h2>I am FLEAR.</h2>
|
||||
<h3>Lead and Manager for Chrome Developer Relations.</h3>
|
||||
<p>I love the web. The web should allow anyone to access any experience that they need without the need for native install or content walled garden.</p>
|
||||
</div>
|
||||
</header>
|
||||
<div>
|
||||
<main>
|
||||
{{ $paginator := .Paginate (where .Site.Pages "Type" "page") }}
|
||||
{{ range $index, $element := $paginator.Pages }}
|
||||
<div class="post">
|
||||
<header class="{{ if .Params.image_header }}with-background{{end}}" style="{{ if .Params.image_header }}background-image: url('{{ .Params.image_header }}'); {{end}}">
|
||||
<h3><a href="{{ .Permalink }}">{{ .Title}}</a></h3>
|
||||
</header>
|
||||
<div class="description">
|
||||
<div class="author">
|
||||
<div>
|
||||
<strong>FLEAR</strong>
|
||||
<span><time pubdate>{{ .Date.Format "January 2 2006" }}</time></span>
|
||||
</div>
|
||||
</div>
|
||||
<p>{{ if .Description }}{{ .Description | html }}{{ else }}{{ .Summary }}{{ end }}</p>
|
||||
<div style="text-align: right;">
|
||||
<a class="read-more" href="{{ .Permalink }}">
|
||||
Read More
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ if eq $index 0 }}
|
||||
|
||||
{{end}}
|
||||
{{ end }}
|
||||
|
||||
<nav style="text-align: center">
|
||||
<div>
|
||||
{{ if .Paginator.HasPrev }}
|
||||
<a href="{{ .Paginator.Prev.URL }}">Previous</a>
|
||||
{{ end }}
|
||||
|
|
||||
{{ if .Paginator.HasNext }}
|
||||
<a href="{{ .Paginator.Next.URL }}">More</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
</nav>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
2
layouts/shortcodes/raw-html.html
Normal file
2
layouts/shortcodes/raw-html.html
Normal file
|
@ -0,0 +1,2 @@
|
|||
<!-- raw html -->
|
||||
{{.Inner}}
|
3
layouts/shortcodes/seperator.html
Normal file
3
layouts/shortcodes/seperator.html
Normal file
|
@ -0,0 +1,3 @@
|
|||
<section style="padding:50px 0" class="hero is--dark">
|
||||
<div class="container text--center"></div>
|
||||
</section>
|
10
layouts/shortcodes/slide.html
Normal file
10
layouts/shortcodes/slide.html
Normal file
|
@ -0,0 +1,10 @@
|
|||
|
||||
<div class=slide>
|
||||
<h4 id="#{{ index .Params 0 }}">{{ index .Params 1 }} <a href="#{{ index .Params 0 }}">#</a></h4>
|
||||
<a href="{{ .Page.Params.slide_images }}{{ index .Params 0 }}.jpg">
|
||||
<img src="{{ .Page.Params.slide_images }}t{{ index .Params 0 }}.jpg" alt="{{ index .Params 1 }}">
|
||||
</a>
|
||||
<p>{{ .Inner }}</p>
|
||||
<br style="clear:both">
|
||||
<hr>
|
||||
</div>
|
4
layouts/shortcodes/tab.html
Normal file
4
layouts/shortcodes/tab.html
Normal file
|
@ -0,0 +1,4 @@
|
|||
<div class="codeBlockWrapper_2QGZ tab-pane" title="{{ .Get 0 }}">
|
||||
{{ .Inner }}
|
||||
<button type="button" aria-label="Copy code to clipboard" class="copyButton_1BYj">Copy</button>
|
||||
</div>
|
15
layouts/shortcodes/tabs.html
Normal file
15
layouts/shortcodes/tabs.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
<div class="ForDevelopers">
|
||||
<div class="row code-tabs">
|
||||
<div class="ButtonTabs col col--3">
|
||||
<div class="nav nav-tabs"></div>
|
||||
</div>
|
||||
<div class="col col--9 code-with-header">
|
||||
<div class="code-with-header">
|
||||
<div class="code-header">{{ .Get 0 }}</div>
|
||||
<div class="scrollbar-container tab-content">
|
||||
{{ .Inner }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
17
layouts/shortcodes/titled-side.html
Normal file
17
layouts/shortcodes/titled-side.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
<header class="hero heroBanner_2Ftp">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col col--5">
|
||||
<h2 class="hero__title">{{ .Get 0 }}</h2>
|
||||
<p class="hero__subtitle">{{ .Get 1 }}</p>
|
||||
<div>
|
||||
<a class="button hero--button button--md button--secondary button--outline responsive-button button_2W20" style="margin-left:0;margin-top:10px" href="{{ .Get 3 }}">{{ .Get 2 }}</a>
|
||||
<a class="button hero--button button--md button--primary responsive-button button_2W20" to="https://git.qoto.org/goblin-ogm/goblin" style="margin-top:10px" href="{{ .Get 5 }}">{{ .Get 4 }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col--7">
|
||||
{{ .Inner }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
11
layouts/shortcodes/titled.html
Normal file
11
layouts/shortcodes/titled.html
Normal file
|
@ -0,0 +1,11 @@
|
|||
<section class="forDevelopers_1bbC">
|
||||
<div class="container">
|
||||
<div class="row responsiveCentered_2A7S">
|
||||
<div class="col col--6 col--offset-3">
|
||||
<h2 class="with-underline">{{ .Get 0 }}</h2>
|
||||
<p>{{ .Get 1 }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{ .Inner }}
|
||||
</div>
|
||||
</section>
|
49
layouts/taxonomy/list.html
Normal file
49
layouts/taxonomy/list.html
Normal file
|
@ -0,0 +1,49 @@
|
|||
{{ partial "top_list_resource.html" . }}
|
||||
|
||||
{{ $paginator := .Paginate .Data.Pages }}
|
||||
<div class='row'>
|
||||
<div class='col-md-8'>
|
||||
<div class='extra-pagination inner'>
|
||||
<nav class='pagination' role='pagination'>
|
||||
{{ if .Paginator.HasNext }}<a href="{{ .Paginator.Next.URL }}" class="newer-posts">← Newer Posts</a>{{ end }}
|
||||
<span class='page-number'>Page {{ .Paginator.PageNumber }} of {{ .Paginator.TotalPages }}</span>
|
||||
{{ if .Paginator.HasPrev }}<a href="{{ .Paginator.Prev.URL }}" class="older-posts">Older Posts →</a>{{ end }}
|
||||
</nav>
|
||||
</div>
|
||||
{{ range $index, $element := $paginator.Pages }}
|
||||
|
||||
<article class='post'>
|
||||
<header class='post-header'>
|
||||
<h2 class='post-title'>
|
||||
<a href="{{ .Permalink }}">{{ .Title}}</a>
|
||||
{{ if .Params.link }} — <a href="{{.Params.link}}">🔗</a>{{end}}
|
||||
</h2>
|
||||
</header>
|
||||
</article>
|
||||
|
||||
{{ end }}
|
||||
|
||||
<div class='extra-pagination inner'>
|
||||
<nav class='pagination' role='pagination'>
|
||||
{{ if .Paginator.HasNext }}<a href="{{ .Paginator.Next.URL }}" class="newer-posts">← Newer Posts</a>{{ end }}
|
||||
<span class='page-number'>Page {{ .Paginator.PageNumber }} of {{ .Paginator.TotalPages }}</span>
|
||||
{{ if .Paginator.HasPrev }}<a href="{{ .Paginator.Prev.URL }}" class="older-posts">Older Posts →</a>{{ end }}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class='col-md-4'>
|
||||
<nav class='menu tags'>
|
||||
<span class="menu-heading">Categories</span>
|
||||
{{range $name, $taxonomy := .Site.Taxonomies.tags}} {{ $cnt := .Count }}
|
||||
{{ with $.Site.GetPage (printf "/tags/%s" $name) }}
|
||||
<a href="{{ .RelPermalink }}" class='menu-item tag' data-slug='{{$name}}' title="All pages with tag <i>{{$name}}</i>">{{$name}} ({{$cnt}})</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</nav>
|
||||
<br>
|
||||
{{ partial "microblog.html" . }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ partial "bottom_list.html" . }}
|
76
layouts/taxonomy/tag.html
Normal file
76
layouts/taxonomy/tag.html
Normal file
|
@ -0,0 +1,76 @@
|
|||
{{ partial "top_list_resource.html" . }}
|
||||
|
||||
{{ $paginator := .Paginate .Data.Pages }}
|
||||
<div class='row'>
|
||||
<div class='col-md-8'>
|
||||
<div class='extra-pagination inner'>
|
||||
<nav class='pagination' role='pagination'>
|
||||
{{ if .Paginator.HasNext }}<a href="{{ .Paginator.Next.URL }}" class="newer-posts">← Newer Posts</a>{{ end }}
|
||||
<span class='page-number'>Page {{ .Paginator.PageNumber }} of {{ .Paginator.TotalPages }}</span>
|
||||
{{ if .Paginator.HasPrev }}<a href="{{ .Paginator.Prev.URL }}" class="older-posts">Older Posts →</a>{{ end }}
|
||||
</nav>
|
||||
</div>
|
||||
{{ range $index, $element := $paginator.Pages }}
|
||||
|
||||
<article class='post'>
|
||||
<header class='post-header'>
|
||||
<h2 class='post-title'>
|
||||
<a href="{{ .Permalink }}">{{ .Title}}</a>
|
||||
{{ if .Params.link }} — <a href="{{.Params.link}}">🔗</a>{{end}}
|
||||
</h2>
|
||||
<section class='post-meta text-muted'>
|
||||
<ul class='list-inline'>
|
||||
<li>
|
||||
<i class='fa fa-calendar'></i>
|
||||
<time class='post-date' datetime='{{ .Date.Format "2020-09-10" }}'> {{ .Date.Format "January 2 2006" }} </time>
|
||||
</li>
|
||||
<li>
|
||||
<a class='fa-solid fa-book-open' href='{{ .Permalink }}'>
|
||||
<span class='hidden'>Book icon</span>
|
||||
</a>
|
||||
<a href='{{ .Permalink }}'>Reading time: {{ .ReadingTime }} minute{{ if (ne .ReadingTime 1) }}s{{ end }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<i class='fa fa-folder-open'></i>
|
||||
{{ $tags := .Params.tags}}
|
||||
{{ with $tags }}
|
||||
{{ range . }}
|
||||
<a href="{{ "/tags/" | relLangURL }}{{ . | urlize }}">{{ . }}</a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</header>
|
||||
<section class='post-content'>
|
||||
{{.Content }}
|
||||
</section>
|
||||
</article>
|
||||
|
||||
{{ end }}
|
||||
|
||||
<div class='extra-pagination inner'>
|
||||
<nav class='pagination' role='pagination'>
|
||||
{{ if .Paginator.HasNext }}<a href="{{ .Paginator.Next.URL }}" class="newer-posts">← Newer Posts</a>{{ end }}
|
||||
<span class='page-number'>Page {{ .Paginator.PageNumber }} of {{ .Paginator.TotalPages }}</span>
|
||||
{{ if .Paginator.HasPrev }}<a href="{{ .Paginator.Prev.URL }}" class="older-posts">Older Posts →</a>{{ end }}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class='col-md-4 side-menu'>
|
||||
<nav class='menu tags'>
|
||||
<span class="menu-heading">Categories</span>
|
||||
{{ $hereLink := .Permalink }}
|
||||
{{range $name, $taxonomy := .Site.Taxonomies.tags}} {{ $cnt := .Count }}
|
||||
{{ with $.Site.GetPage (printf "/tags/%s" $name) }}
|
||||
<a href="{{ .RelPermalink }}" class='menu-item tag{{ if (eq .Permalink $hereLink) }} selected{{ end }}' data-slug='{{$name}}' title="All pages with tag <i>{{$name}}</i>">{{$name}} ({{$cnt}})</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</nav>
|
||||
<br>
|
||||
{{ partial "microblog.html" . }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ partial "bottom_list.html" . }}
|
20
lib/activitypub/utils/fetchActorInformation.ts
Normal file
20
lib/activitypub/utils/fetchActorInformation.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { AP } from "activitypub-core-types";
|
||||
|
||||
export async function fetchActorInformation(actorUrl: string): Promise<AP.Actor | null> {
|
||||
try {
|
||||
const response = await fetch(
|
||||
actorUrl,
|
||||
{
|
||||
headers: {
|
||||
"Content-type": 'application/activity+json',
|
||||
"Accept": 'application/activity+json'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.log("Unable to fetch action information", actorUrl);
|
||||
}
|
||||
return null;
|
||||
}
|
7
lib/activitypub/utils/parseSignature.ts
Normal file
7
lib/activitypub/utils/parseSignature.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { VercelRequest } from '@vercel/node';
|
||||
import parser from '../../http-signature/index.js';
|
||||
|
||||
export function parseSignature(request: VercelRequest) {
|
||||
const { url, method, headers } = request;
|
||||
return parser.parse({ url, method, headers });
|
||||
}
|
44
lib/activitypub/utils/sendSignedRequest.ts
Normal file
44
lib/activitypub/utils/sendSignedRequest.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { AP } from 'activitypub-core-types';
|
||||
import { Sha256Signer } from '../../http-signature/index.js';
|
||||
import { createHash } from 'crypto';
|
||||
|
||||
export async function sendSignedRequest(endpoint: URL, object: AP.Activity): Promise<Response> {
|
||||
const publicKeyId = "https://flear.org/flear#main-key";
|
||||
const privateKey = process.env.ACTIVITYPUB_PRIVATE_KEY;
|
||||
|
||||
const signer = new Sha256Signer({ publicKeyId, privateKey, headerNames: ["host", "date", "digest"] });
|
||||
|
||||
const requestHeaders = {
|
||||
host: endpoint.hostname,
|
||||
date: new Date().toUTCString(),
|
||||
digest: `SHA-256=${createHash('sha256').update(JSON.stringify(object)).digest('base64')}`
|
||||
};
|
||||
|
||||
// Generate the signature header
|
||||
const signature = signer.sign({
|
||||
url: endpoint,
|
||||
method: "POST",
|
||||
headers: requestHeaders
|
||||
});
|
||||
|
||||
console.log("object", object);
|
||||
console.log("endpoint", endpoint);
|
||||
console.log("requestHeaders", requestHeaders);
|
||||
console.log("signature", signature);
|
||||
|
||||
const response = await fetch(
|
||||
endpoint,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(object),
|
||||
headers: {
|
||||
'content-type': "application/activity+json",
|
||||
accept: "application/activity+json",
|
||||
...requestHeaders,
|
||||
signature: signature
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
21
lib/activitypub/utils/testVerifySignature.js
Normal file
21
lib/activitypub/utils/testVerifySignature.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { Parser, Sha256Signer, Sha256Signature, Signature } from '../../http-signature/index.js';
|
||||
|
||||
const publicKeyPem = `-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuPKVdZpt/olFWtyC4tpJ
|
||||
bAoxyuFAQ7VDHNjDtC6ooirEtSzAnljaMcw3PLM0KiOTRUoZOeg2GRR4M8mBV2Lv
|
||||
RdYYMnlFAJNXCTJ0uW3UxVFR5FrMeHlwA27zhwZpaQ74Daz8MTwZ+TFsJoA1UGJl
|
||||
V3HKZ4QpVYrYNSKl6K75p3+EM/c8AlIA0BuFzJVZYsT3Q9ZJgJCDBXKhqBNEBRSt
|
||||
6LwOONjzcQkzA9t6l6D0Hwzhb4ySspwH2Ur2X+QPisPnrc01KT3YTX/ygPDDnDcR
|
||||
rSI+ehs+LtVPqPoAfYMzIKhlo4WLB5gmObO3oQRlg7kda0pFI9FGhLrwV0R3h0BT
|
||||
kwIDAQAB
|
||||
-----END PUBLIC KEY-----`;
|
||||
|
||||
const publicKeyId = "https://flear.org/flear#main-key";
|
||||
|
||||
const signature = new Sha256Signature({
|
||||
publicKeyId,
|
||||
string: 'host: qoto.org\ndate: Fri, 14 Jul 2023 20:36:24 GMT\ndigest: SHA-256=q0ubZcImm4TpDMjk6Q+geJZAsjgODR1m5V/pqcXFSdM=',
|
||||
signature: Buffer.from("IiqUSA2oXSzNTsMJE7jOP7YDnL6K7Nol0rpDf+dnK8R14Y6FXX9VdSW43JTDvtnrmlf/lROR36pXXdf/IQhKtHkEJjKQUviBM5BhA9Qv5S84gGXyNkx2ytXEmxcL7BK2nuS/QoW9ud99ZcmhGkzWraoGQ7BM5UV63OCfA3EkKT0gP/QN76eMtuKVVQwCTNZtVxq/RBaJgExrn4+XwaWqFIBovVRM6p+3Pbg1T0e6Eo3Lsy6yn0Um7+iceyjVtveGiV60ywy9bkf85DzoFSSfB4y7sOhjartYyuuBY8HBRkheEywoJH1cK/q29F1Z6jByx84SrlD925sfTgOsB67DNw==")
|
||||
})
|
||||
|
||||
console.log(signature.verify(publicKeyPem));
|
14
lib/activitypub/utils/verifySignature.ts
Normal file
14
lib/activitypub/utils/verifySignature.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
export function verifySignature(signature:any, publicKeyJson:any) {
|
||||
let signatureValid;
|
||||
|
||||
try {
|
||||
// Verify the signature
|
||||
signatureValid = signature.verify(
|
||||
publicKeyJson.publicKeyPem
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("Signature Verification error", error);
|
||||
}
|
||||
|
||||
return signatureValid;
|
||||
}
|
215
lib/http-signature/index.js
Normal file
215
lib/http-signature/index.js
Normal file
|
@ -0,0 +1,215 @@
|
|||
/** PK Fixed version.
|
||||
* Activitypub HTTP Signatures
|
||||
* Based on [HTTP Signatures draft 08](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-08)
|
||||
* @module activitypub-http-signatures
|
||||
*/
|
||||
|
||||
import crypto from 'crypto';
|
||||
|
||||
|
||||
// token definitions from definitions in rfc7230 and rfc7235
|
||||
const token = String.raw`[!#$%&'\*+\-\.\^_\`\|~0-9a-z]+`; // Key or value
|
||||
const qdtext = String.raw`[^"\\\x7F]`; // Characters that don't need escaping
|
||||
const quotedPair = String.raw`\\[\t \x21-\x7E\x80-\xFF]`; // Escaped characters
|
||||
const quotedString = `(?:${qdtext}|${quotedPair})*`;
|
||||
const fieldMatch = new RegExp(String.raw`(?<=^|,\s*)(${token})\s*=\s*(?:(${token})|"(${quotedString})")(?=,|$)`, 'ig');
|
||||
const parseSigFields = str => Object.fromEntries(
|
||||
Array.from(
|
||||
str.matchAll(fieldMatch)
|
||||
).map(
|
||||
// capture groups: 1=fieldname, 2=field value if unquoted, 3=field value if quoted
|
||||
v=>[
|
||||
v[1],
|
||||
v[2] ?? v[3].replace(
|
||||
/\\./g,
|
||||
c=>c[1]
|
||||
|
||||
)
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
const defaultHeaderNames = ['(request-target)', 'host', 'date'];
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Generate the string to be signed for the signature header
|
||||
* @param {Object} options Options
|
||||
* @param {string} options.target The pathname of the request URL (including query and hash strings)
|
||||
* @param {string} options.method The HTTP request method
|
||||
* @param {object} options.headers Object whose keys are http header names and whose values are those headers' values
|
||||
* @param {string[]} headerNames Names of the headers to use in the signature
|
||||
* @returns {string}
|
||||
*/
|
||||
function getSignString({ target, method, headers }, headerNames) {
|
||||
const requestTarget = `${method.toLowerCase()} ${target}`;
|
||||
headers = {
|
||||
...headers,
|
||||
'(request-target)': requestTarget
|
||||
};
|
||||
return headerNames.map(header => `${header.toLowerCase()}: ${headers[header]}`).join('\n');
|
||||
}
|
||||
|
||||
export class Sha256Signer {
|
||||
#publicKeyId;
|
||||
#privateKey;
|
||||
#headerNames;
|
||||
|
||||
/**
|
||||
* Class for signing a request and returning the signature header
|
||||
* @param {object} options Config options
|
||||
* @param {string} options.publicKeyId URI for public key that must be used for verification
|
||||
* @param {string} options.privateKey Private key to use for signing
|
||||
* @param {string[]} options.headerNames Names of headers to use in generating signature
|
||||
*/
|
||||
constructor({ publicKeyId, privateKey, headerNames }) {
|
||||
this.#publicKeyId = publicKeyId;
|
||||
this.#privateKey = privateKey;
|
||||
this.#headerNames = headerNames ?? defaultHeaderNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the signature header for an outgoing message
|
||||
* @param {object} reqOptions Request options
|
||||
* @param {string} reqOptions.url The full URL of the request to sign
|
||||
* @param {string} reqOptions.method Method of the request
|
||||
* @param {object} reqOptions.headers Dict of headers used in the request
|
||||
* @returns {string} Value for the signature header
|
||||
*/
|
||||
sign({ url, method, headers }) {
|
||||
const { host, pathname, search } = new URL(url);
|
||||
const target = `${pathname}${search}`;
|
||||
headers.date = headers.date || new Date().toUTCString();
|
||||
headers.host = headers.host || host;
|
||||
|
||||
const headerNames = this.#headerNames;
|
||||
|
||||
const stringToSign = getSignString({ target, method, headers }, headerNames);
|
||||
|
||||
const signature = this.#signSha256(this.#privateKey, stringToSign).toString('base64');
|
||||
|
||||
return `keyId="${this.#publicKeyId}",headers="${headerNames.join(' ')}",signature="${signature.replace(/"/g, '\\"')}",algorithm="rsa-sha256"`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Sign a string with a private key using sha256 alg
|
||||
* @param {string} privateKey Private key
|
||||
* @param {string} stringToSign String to sign
|
||||
* @returns {Buffer} Signature buffer
|
||||
*/
|
||||
#signSha256(privateKey, stringToSign) {
|
||||
const signer = crypto.createSign('sha256');
|
||||
signer.update(stringToSign);
|
||||
const signature = signer.sign(privateKey);
|
||||
signer.end();
|
||||
return signature;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Incoming request parser and Signature factory.
|
||||
* If you wish to support more signature types you can extend this class
|
||||
* and overide getSignatureClass.
|
||||
*/
|
||||
export class Parser {
|
||||
/**
|
||||
* Parse an incomming request's http signature header
|
||||
* @param {object} reqOptions Request options
|
||||
* @param {string} reqOptions.url The pathname (and query string) of the request URL
|
||||
* @param {string} reqOptions.method Method of the request
|
||||
* @param {object} reqOptions.headers Dict of headers used in the request
|
||||
* @returns {Signature} Object representing the signature
|
||||
* @throws {UnkownAlgorithmError} If the algorithm used isn't one we know how to verify
|
||||
*/
|
||||
parse({ headers, method, url }){
|
||||
const fields = parseSigFields(headers.signature);
|
||||
const headerNames = (fields.headers ?? 'date').split(/\s+/);
|
||||
const signature = Buffer.from(fields.signature, 'base64');
|
||||
const signString = getSignString({ target: url, method, headers }, headerNames);
|
||||
const keyId = fields.keyId;
|
||||
const algorithm = fields.algorithm ?? 'rsa-sha256';
|
||||
|
||||
return this.getSignatureClass(algorithm, { signature, string: signString, keyId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct the signature class for a given algorithm.
|
||||
* Override this method if you want to support additional
|
||||
* algorithms.
|
||||
* @param {string} algorithm The algorithm used by the signed request
|
||||
* @param {object} options
|
||||
* @param {Buffer} options.signature The signature as a buffer
|
||||
* @param {string} options.string The string that was signed
|
||||
* @param {string} options.keyId The ID of the public key to be used for verification
|
||||
* @returns {Signature}
|
||||
* @throws {UnkownAlgorithmError} If an unknown algorithm was used
|
||||
*/
|
||||
getSignatureClass(algorithm, { signature, string, keyId }) {
|
||||
if(algorithm === 'rsa-sha256') {
|
||||
return new Sha256Signature({ signature, string, keyId });
|
||||
} else {
|
||||
throw new UnkownAlgorithmError(`Don't know how to verify ${algorithm} signatures.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class UnkownAlgorithmError extends Error {}
|
||||
|
||||
export class Signature {
|
||||
#keyId;
|
||||
|
||||
constructor(keyId) {
|
||||
this.#keyId = keyId;
|
||||
}
|
||||
|
||||
get keyId(){
|
||||
return this.#keyId;
|
||||
}
|
||||
|
||||
verify(key){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class Sha256Signature extends Signature {
|
||||
#signature;
|
||||
#string;
|
||||
|
||||
/**
|
||||
* Class representing the HTTP signature
|
||||
* @param {object} options
|
||||
* @param {Buffer} options.signature The signature as a buffer
|
||||
* @param {string} options.string The string that was signed
|
||||
* @param {string} options.keyId The ID of the public key to be used for verification
|
||||
*/
|
||||
constructor({ signature, string, keyId }) {
|
||||
super(keyId);
|
||||
this.#signature = signature;
|
||||
this.#string = string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @property {string} keyId The ID of the public key that can verify the signature
|
||||
*/
|
||||
|
||||
/**
|
||||
* Verify the signature using a public key
|
||||
* @param {string} key The public key matching the signature's keyId
|
||||
* @returns {boolean}
|
||||
*/
|
||||
verify(key) {
|
||||
const signature = this.#signature;
|
||||
const signedString = this.#string;
|
||||
const verifier = crypto.createVerify('sha256');
|
||||
verifier.write(signedString);
|
||||
verifier.end();
|
||||
|
||||
return verifier.verify(key, signature);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default export: new instance of Parser class
|
||||
*/
|
||||
export default new Parser;
|
73
lib/whatwg-flora.ts
Normal file
73
lib/whatwg-flora.ts
Normal file
|
@ -0,0 +1,73 @@
|
|||
const encoder = new TextEncoder();
|
||||
|
||||
const pipeInto = async (from:any, controller:any) => {
|
||||
const reader = from.getReader();
|
||||
|
||||
return reader.read().then(function process(result:any) {
|
||||
if (result.done) {
|
||||
return;
|
||||
}
|
||||
const value = result.value;
|
||||
const isTypedArray = (value instanceof Int8Array
|
||||
|| value instanceof Int16Array
|
||||
|| value instanceof Int32Array
|
||||
|| value instanceof Uint8Array
|
||||
|| value instanceof Uint8ClampedArray
|
||||
|| value instanceof Uint16Array
|
||||
|| value instanceof Uint32Array
|
||||
|| value instanceof Float32Array
|
||||
|| value instanceof Float64Array)
|
||||
if((isTypedArray === false && !!result.value) || (isTypedArray && value.length > 0)) {
|
||||
controller.enqueue(result.value);
|
||||
}
|
||||
return reader.read().then(process);
|
||||
});
|
||||
};
|
||||
|
||||
const enqueueItem = async (val:any, controller:any) => {
|
||||
if (val instanceof globalThis.ReadableStream) {
|
||||
await pipeInto(val, controller);
|
||||
}
|
||||
else if (val instanceof Promise) {
|
||||
let newVal;
|
||||
newVal = await val;
|
||||
|
||||
if (newVal instanceof globalThis.ReadableStream) {
|
||||
await pipeInto(newVal, controller);
|
||||
} else {
|
||||
await enqueueItem(newVal, controller);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (Array.isArray(val)) {
|
||||
for (let item of val) {
|
||||
await enqueueItem(item, controller)
|
||||
}
|
||||
}
|
||||
else if (!!val) {
|
||||
controller.enqueue(encoder.encode(val));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default async (strings: TemplateStringsArray, ...values: any) => {
|
||||
return new globalThis.ReadableStream({
|
||||
start(controller) {
|
||||
async function push() {
|
||||
let i = 0;
|
||||
while (i < values.length) {
|
||||
let html = strings[i];
|
||||
|
||||
controller.enqueue(encoder.encode(html));
|
||||
await enqueueItem(values[i], controller);
|
||||
|
||||
i++;
|
||||
}
|
||||
controller.enqueue(encoder.encode(strings[i]));
|
||||
controller.close();
|
||||
}
|
||||
|
||||
push();
|
||||
}
|
||||
});
|
||||
};
|
12375
package-lock.json
generated
Normal file
12375
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
32
package.json
Normal file
32
package.json
Normal file
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"name": "flear.org",
|
||||
"description": "FLEAR (Free and Libre Engineers for Amateur Radio)",
|
||||
"dependencies": {
|
||||
"@firebase/app-types": "^0.9.0",
|
||||
"@polymath-ai/client": "*",
|
||||
"@polymath-ai/host": "*",
|
||||
"@vercel/analytics": "^1.0.2",
|
||||
"@vercel/edge": "^1.1.0",
|
||||
"@vercel/node": "^3.0.7",
|
||||
"@vercel/og": "^0.5.17",
|
||||
"activitypub-core-types": "^0.3.2",
|
||||
"ava": "^5.3.1",
|
||||
"file-drop-element": "^1.0.1",
|
||||
"firebase-admin": "^11.11.0",
|
||||
"hugo-bin": "^0.115.0",
|
||||
"multiparty": "^4.2.3",
|
||||
"node-fetch": "^3.3.2",
|
||||
"pinch-zoom-element": "^1.1.1",
|
||||
"uuid": "^9.0.1",
|
||||
"vercel": "^32.4.1",
|
||||
"whatwg-flora-tmpl": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/multiparty": "^0.0.34",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
16
static/blank.html
Normal file
16
static/blank.html
Normal file
|
@ -0,0 +1,16 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<!-- import the webpage's javascript file -->
|
||||
<style>
|
||||
html, body {
|
||||
margin:0;
|
||||
padding:0;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
</html>
|
BIN
static/favicon.png
Normal file
BIN
static/favicon.png
Normal file
Binary file not shown.
BIN
static/fonts/fa-brands-400.ttf
Normal file
BIN
static/fonts/fa-brands-400.ttf
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue