Add homepage

This commit is contained in:
Daniele Tonon
2023-09-11 07:48:38 +02:00
parent c4bad18ee5
commit 67599a696f
9 changed files with 370 additions and 24 deletions

View File

@@ -61,6 +61,7 @@ func main() {
// initialize templates
// use a mapping to expressly link the templates and share them between more kinds/types
templateMapping["homepage"] = "homepage.html"
templateMapping["profile"] = "profile.html"
templateMapping["profile_sitemap"] = "sitemap.xml"
templateMapping["note"] = "note.html"
@@ -88,6 +89,8 @@ func main() {
)
// routes
http.HandleFunc("/", render)
http.HandleFunc("/try", renderTry)
http.HandleFunc("/robots.txt", renderRobots)
http.HandleFunc("/njump/image/", generate)
http.HandleFunc("/njump/proxy/", proxy)
@@ -96,7 +99,6 @@ func main() {
http.HandleFunc("/relays-archive/", renderArchive)
http.HandleFunc("/npubs-archive.xml", renderArchive)
http.HandleFunc("/relays-archive.xml", renderArchive)
http.HandleFunc("/", render)
log.Print("listening at http://0.0.0.0:" + s.Port)
if err := http.ListenAndServe("0.0.0.0:"+s.Port, nil); err != nil {

View File

@@ -66,7 +66,7 @@ func render(w http.ResponseWriter, r *http.Request) {
}
if code == "" {
fmt.Fprintf(w, "call /<nip19 code>")
renderHomepage(w, r)
return
}

48
render_homepage.go Normal file
View File

@@ -0,0 +1,48 @@
package main
import (
"context"
"net/http"
"time"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip19"
)
func renderHomepage(w http.ResponseWriter, r *http.Request) {
typ := "homepage"
w.Header().Set("Cache-Control", "max-age=3600")
npubsHex := cache.GetPaginatedkeys("pa", 1, 50)
npubs := []string{}
for i := 0; i < len(npubsHex); i++ {
npub, _ := nip19.EncodePublicKey(npubsHex[i])
npubs = append(npubs, npub)
}
ctx, cancel := context.WithTimeout(r.Context(), time.Second*5)
defer cancel()
var lastEvents []*nostr.Event
if relay, err := pool.EnsureRelay("nostr.wine"); err == nil {
lastEvents, _ = relay.QuerySync(ctx, nostr.Filter{
Kinds: []int{1},
Limit: 50,
})
}
lastNotes := []string{}
relay := []string{"wss://nostr.wine"}
for _, n := range lastEvents {
nevent, _ := nip19.EncodeEvent(n.ID, relay, n.PubKey)
lastNotes = append(lastNotes, nevent)
}
params := map[string]any{
"npubs": npubs,
"lastNotes": lastNotes,
}
if err := tmpl.ExecuteTemplate(w, templateMapping[typ], params); err != nil {
log.Error().Err(err).Msg("error rendering")
return
}
}

15
render_try.go Normal file
View File

@@ -0,0 +1,15 @@
package main
import (
"net/http"
)
func renderTry(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
http.Error(w, "Failed to parse form data", http.StatusBadRequest)
return
}
nip19entity := r.FormValue("nip19entity")
http.Redirect(w, r, "/"+nip19entity, http.StatusFound)
}

View File

@@ -250,6 +250,70 @@ iframe {
width: 100%;
}
}
.container .try {
padding: 2rem 2rem 1.4rem 2rem;
border-radius: 8px;
font-size: 0.8rem;
}
.theme--default .container .try {
background-color: #f3f3f3;
}
.theme--dark .container .try {
background-color: #2d2d2d;
}
@media (max-width: 580px) {
.container .try {
padding: 1rem 1rem 0.8rem 1rem;
}
}
.container .try .tryForm {
display: flex;
}
.container .try .tryForm input, .container .try .tryForm button {
font-size: 1.2rem;
padding: 0.5rem;
flex-basis: 90%;
border-radius: 8px;
}
.theme--default .container .try .tryForm input {
color: #373737;
background-color: #ffffff;
border: 1px solid #c9c9c9;
}
.theme--dark .container .try .tryForm input {
color: #fafafa;
background-color: #1e1e1e;
border: 1px solid #969696;
}
.container .try .tryForm input:focus {
outline: none; /* Remove the default outline */
}
.theme--default .container .try .tryForm ::placeholder {
color: #c9c9c9;
}
.theme--dark .container .try .tryForm ::placeholder {
color: #969696;
}
.container .try .tryForm button {
flex-basis: 10%;
}
.theme--default .container .try .tryForm button {
background-color: #e32a6d;
color: #ffffff;
border: none;
margin-left: -16px;
}
.theme--dark .container .try .tryForm button {
background-color: #e32a6d;
color: #ffffff;
border: none;
margin-left: -16px;
}
.container .try #pickRandomEntityLink {
display: block;
padding-left: 0.4rem;
margin-top: 0.2rem;
}
.container .columnA {
position: -webkit-sticky;
position: sticky;
@@ -876,6 +940,39 @@ iframe {
}
}
body.homepage .container_wrapper {
display: block;
}
@media (max-width: 580px) {
body.homepage .container_wrapper {
display: block;
padding: 0 1rem;
margin: 0 auto;
}
}
body.homepage .container_wrapper .container {
display: block;
width: 60%;
margin: 0 auto;
}
@media (max-width: 580px) {
body.homepage .container_wrapper .container {
display: block;
width: 100%;
}
}
body.homepage .container_wrapper .container span.exampleUrl {
color: #ffffff;
padding: 0 0.4rem;
border-radius: 4px;
}
.theme--default body.homepage .container_wrapper .container span.exampleUrl {
background-color: #e32a6d;
}
.theme--dark body.homepage .container_wrapper .container span.exampleUrl {
background-color: #e32a6d;
}
body.profile .column_content {
flex-basis: 50%;
max-width: 50%;

View File

@@ -290,6 +290,56 @@ iframe {
width: 100%;
}
.try {
@include themed() {
background-color: t($over-bg);
}
padding: 2rem 2rem 1.4rem 2rem;
border-radius: 8px;
font-size: 0.8rem;
@media (max-width: 580px) {
padding: 1rem 1rem 0.8rem 1rem;
}
.tryForm {
display: flex;
input, button {
font-size: 1.2rem;
padding: 0.5rem;
flex-basis: 90%;
border-radius: 8px;
}
input {
@include themed() {
color: t($base7);
background-color: t($bg-up);
border: 1px solid t($base4);
}
&:focus {
outline: none; /* Remove the default outline */
}
}
::placeholder {
@include themed() {
color: t($base4);
}
}
button {
flex-basis: 10%;
@include themed() {
background-color: t($accent1);
color: $color-base1;
border: none;
margin-left: -16px;
}
}
}
#pickRandomEntityLink {
display: block;
padding-left: 0.4rem;
margin-top: 0.2rem;
}
}
.columnA {
position: -webkit-sticky;
position: sticky;
@@ -802,6 +852,37 @@ iframe {
}
}
body.homepage {
.container_wrapper {
display: block;
@media (max-width: 580px) {
display: block;
padding: 0 1rem;
margin: 0 auto;
}
.container {
display: block;
width: 60%;
margin: 0 auto;
@media (max-width: 580px) {
display: block;
width: 100%;
}
span.exampleUrl {
@include themed() {
background-color: t($accent1);
}
color: $color-base1;
padding: 0 0.4rem;
border-radius: 4px;
}
}
}
}
body.profile {
.column_content {
flex-basis: 50%;

View File

@@ -1,2 +1,2 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/njump/static/styles.css?v=20230902" />
<link rel="stylesheet" href="/njump/static/styles.css?v=20230911" />

99
templates/homepage.html Normal file
View File

@@ -0,0 +1,99 @@
<!DOCTYPE html>
<html class="theme--default">
<meta charset="UTF-8" />
<head>
<title>Njump - The Nostr static gateway</title>
<meta name="description" content="" >
{{template "head_common.html" }}
</head>
<body class="homepage">
{{template "top.html" .}}
<div class="container_wrapper">
<div class="container">
<div>
<h2>What is Njump?</h2>
<p>Njump is a static Nostr gateway that allows you to browse profiles, notes and relays; it is an easy way to preview a resource and then open it with the preferred client.</p>
<p>Njump currently lives under njump.me, you can reach it appending a nostr (<a href="https://github.com/nostr-protocol/nips/blob/master/19.md">NIP-19</a>) entity (npub, nevent, naddr, etc) after the domain: <span class="exampleUrl">njump.me/p/&lt;nip-19-entity&gt;</span><br/>
For example <a href='https://njump.me/npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6'>a user</a> <a href="https://njump.me/npub10000003zmk89narqpczy4ff6rnuht2wu05na7kpnh3mak7z2tqzsv8vwqk">profile</a>, <a href='https://njump.me/nevent1qqs860kwt3m500hfnve6vxdpagkfqkm6hq03dnn2n7u8dev580kd2uszyztuwzjyxe4x2dwpgken87tna2rdlhpd02va5cvvgrrywpddnr3jydc2w4t'>a note</a> or a <a href='https://njump.me/naddr1qqxnzd3cxqmrzv3exgmr2wfeqy08wumn8ghj7mn0wd68yttsw43zuam9d3kx7unyv4ezumn9wshszyrhwden5te0dehhxarj9ekk7mf0qy88wumn8ghj7mn0wvhxcmmv9uq3zamnwvaz7tmwdaehgu3wwa5kuef0qy2hwumn8ghj7un9d3shjtnwdaehgu3wvfnj7q3qdergggklka99wwrs92yz8wdjs952h2ux2ha2ed598ngwu9w7a6fsxpqqqp65wy2vhhv'>long blog post</a></p>
<p>The typical use of njump is to share a resource outside the nostr world (clients), where the nostr: schema is not (yet) active. There are several reasons to prefer njump to share a nostr resource, versus other web clients, let's see them.</p>
<h2>Try it now, jump to a nostr content</h2>
<div class="try">
<form action="/try" method="POST">
<div class="tryForm">
<input id="nip19entity" name="nip19entity" type="text" placeholder="Paste a npub / nprofile / nevent / ..." autofocus /><button>GO</button>
</div>
</form>
<div id="pickRandomEntityLink">or pick a <a href="#">random content</a></div>
</div>
<h2>Clean, fast and solid</h2>
<p>Pages by njump are extremely light and fast to load because there isn't any client side javascript involved; they are minimalistic with the right attention to typography, focusing the content without unecessary details. Furthermore they are cached, so sharing a page you can expect the other part will load it without any glitch in a fraction of second: the perfect tool to onboard new users!</p>
<h2>Good preview</h2>
Njump previews notes in a simple but effective way, including links (to other nostr resources and web), images, video, quotes, code. It is compatible with long form content so it also renders markdown. It shows the note parent, allowing to follow it up. It has custom css for printing or exporting to PDF, so it is a nice option to read long form contents offline.
<h2>Cooperative (jump-out)</h2>
<p>Njump is not interested into "capture" users at all, on the contrary it invites them to "jump" to the nostr resource with one of the proposed clients. It even remembers the most used one and put it on the top for fast click/tap.</p>
<h2>Search engine friendly (jump-in)</h2>
<p>This is crucial: njump pages are static so search engines can index them, these means that njump can help others to discover great content on nostr, jump in and join us! Njump is the only nostr resource that has this explicit goal, if you care that a good note could be found online use njump to share it, this way you also help nostr flourish.</p>
<h2>Bonus: NIP-5 profiles</h2>
Now you can share your own profile with an pretty <a href="https://github.com/nostr-protocol/nips/blob/master/05.md">NIP-05</a> inspired permalink: <span class="exampleUrl">njump.me/p/&lt;nip-5&gt;</span>, example: <a href="https://njump.me/p/fiatjaf.com">https://njump.me/p/fiatjaf.com</a>
A profile shows the basic metadata infos, the used "outbox" relays (Gossip model) and the last notes.
Of course profiles are also static, fast and indexable, so start to promote your nostr presence this way!
<h2>Bonus 2: relays</h2>
You can have a view of the last content posted to a relay using <span class="exampleUrl">njump.me/r/&lt;relay-host&gt;</span>, example: <a href="https://njump.me/r/nostr.wine">https://njump.me/r/nostr.wine</a>
Some basic infos (<a href="https://github.com/nostr-protocol/nips/blob/master/11.md">NIP-11</a>) are available; I hope operators will start to make them more personal and informative so users can have a way to evaluate if/when to join a relay.
<h2>Bonus 3: Inspector tool</h2>
You know, we are all devs including our moms, so for every njump resource you can toggle the "Show more details" switch and inspect the full event's json; without installing other tools, like nak, this is probably the fastest way to obtain it.
</div>
</div>
</div>
{{template "footer.html"}}
<script>
// JavaScript object with a list of 50 names
const entitiesList = [
{{range $element := .npubs }}
"{{ $element | escapeString}}",
{{end}}
{{range $element := .lastNotes }}
"{{ $element | escapeString}}",
{{end}}
];
// Function to generate a random name from the list
function pickRandomEntity(event) {
event.preventDefault();
const randomIndex = Math.floor(Math.random() * entitiesList.length);
const randomEntity = entitiesList[randomIndex];
document.getElementById("nip19entity").value = randomEntity;
}
// Add a click event listener to the link
const pickRandomEntityLink = document.getElementById("pickRandomEntityLink");
pickRandomEntityLink.addEventListener("click", pickRandomEntity);
{{template "scripts.js"}}
</script>
</body>
</html>

View File

@@ -43,15 +43,17 @@ for (let i = 0; i < jsons.length; i++) {
}
const shareButton = document.querySelector('.open-list')
const clients_list = document.querySelector('.column_clients')
shareButton.addEventListener('click', function () {
clients_list.classList.toggle('up')
if (clients_list.classList.contains('up')) {
document.body.classList.add('lock')
} else {
document.body.classList.remove('lock')
}
})
if (shareButton) {
const clients_list = document.querySelector('.column_clients')
shareButton.addEventListener('click', function () {
clients_list.classList.toggle('up')
if (clients_list.classList.contains('up')) {
document.body.classList.add('lock')
} else {
document.body.classList.remove('lock')
}
})
}
function updateAdvanceSwitch() {
advanced_list.forEach(element => {
@@ -125,18 +127,20 @@ document.addEventListener('DOMContentLoaded', function () {
})
const desktop_name = document.querySelector('.column_content .name');
window.addEventListener('scroll', function() {
desktop_profile = document.querySelector('.column_content .info-wrapper');
if (window.getComputedStyle(desktop_profile).display === 'none') {
return
}
columnA = document.querySelector('.columnA')
if (columnA != null && isElementInViewport(desktop_name)) {
columnA.querySelector('.info-wrapper').style.display = 'none';
} else {
document.querySelector('.info-wrapper').style.display = 'block';
}
});
if (desktop_name) {
window.addEventListener('scroll', function() {
desktop_profile = document.querySelector('.column_content .info-wrapper');
if (window.getComputedStyle(desktop_profile).display === 'none') {
return
}
columnA = document.querySelector('.columnA')
if (columnA != null && isElementInViewport(desktop_name)) {
columnA.querySelector('.info-wrapper').style.display = 'none';
} else {
document.querySelector('.info-wrapper').style.display = 'block';
}
});
}
// Needed to apply proper print styles
if (