SSR: Repost ohne JS, return_to in Formularen, Werbebanner entfernt
Letzter vollständiger SSR-Stand vor dem Rückbau auf reine SPA. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TiXsPUqw7oeomZ8wZrQW5q
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c66df283d1
commit
eccddcebe6
+111
-3
@@ -288,6 +288,85 @@ func TestBump(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestRepostOhneJavaScript deckt den Repost-Weg ohne JS ab: der Knopf muss im
|
||||
// Feed stehen, der Klick per 303 an den Beitrag zurückführen, danach gesperrt
|
||||
// sein -- und ein erneuter Klick im Cooldown eine HTML-Seite liefern, nicht die
|
||||
// JSON-Zeile, die ein Browser roh anzeigen würde.
|
||||
func TestRepostOhneJavaScript(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
alice := registerAndLogin(t, srv, "alice")
|
||||
pid := createEntry(t, alice, srv, "hebt euch")
|
||||
|
||||
// Angemeldet: Repost-Formular mit Rücksprungziel, Knopf noch frei.
|
||||
_, page := getHTML(t, alice, srv.URL+"/feed")
|
||||
if !strings.Contains(page, fmt.Sprintf(`action="/entry/%d/bump"`, pid)) {
|
||||
t.Fatalf("kein repost-formular im feed:\n%s", excerpt(page, "interactions"))
|
||||
}
|
||||
if !strings.Contains(page, "Repost (0)") {
|
||||
t.Errorf("repost-zähler falsch:\n%s", excerpt(page, "bump"))
|
||||
}
|
||||
if strings.Contains(page, `class="bump" type="submit" disabled`) {
|
||||
t.Errorf("repost-knopf ohne bisherigen repost gesperrt:\n%s", excerpt(page, "bump"))
|
||||
}
|
||||
|
||||
// Reposten -> 303 zurück an den Beitrag.
|
||||
resp := postFormHTML(t, alice, fmt.Sprintf("%s/entry/%d/bump", srv.URL, pid),
|
||||
url.Values{"return_to": {fmt.Sprintf("/feed#e%d", pid)}})
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusSeeOther {
|
||||
t.Fatalf("repost: erwartet 303, bekam %d", resp.StatusCode)
|
||||
}
|
||||
if got, want := resp.Header.Get("Location"), fmt.Sprintf("/feed#e%d", pid); got != want {
|
||||
t.Fatalf("repost: Location = %q, erwartet %q", got, want)
|
||||
}
|
||||
|
||||
// Jetzt im Cooldown: Zähler hoch, Knopf gesperrt, Wartezeit sichtbar.
|
||||
_, page = getHTML(t, alice, srv.URL+"/feed")
|
||||
if !strings.Contains(page, "Repost (1)") {
|
||||
t.Errorf("repost-zähler nicht erhöht:\n%s", excerpt(page, "bump"))
|
||||
}
|
||||
if !strings.Contains(page, `class="bump" type="submit" disabled`) {
|
||||
t.Errorf("repost-knopf im cooldown nicht gesperrt:\n%s", excerpt(page, "bump"))
|
||||
}
|
||||
|
||||
// Erneuter Repost im Cooldown -> HTML-Fehlerseite, nicht rohes JSON.
|
||||
resp = postFormHTML(t, alice, fmt.Sprintf("%s/entry/%d/bump", srv.URL, pid), url.Values{})
|
||||
cooldown := body(t, resp)
|
||||
if resp.StatusCode != http.StatusTooManyRequests {
|
||||
t.Fatalf("repost im cooldown: erwartet 429, bekam %d", resp.StatusCode)
|
||||
}
|
||||
if ct := resp.Header.Get("Content-Type"); !strings.HasPrefix(ct, "text/html") {
|
||||
t.Fatalf("repost im cooldown: erwartet HTML, bekam %q", ct)
|
||||
}
|
||||
if !strings.Contains(cooldown, "erst kürzlich repostet") {
|
||||
t.Errorf("keine verständliche meldung:\n%s", cooldown)
|
||||
}
|
||||
|
||||
// Der JSON-Client bekommt weiterhin JSON mit Nutzdaten.
|
||||
resp = postForm(t, alice, fmt.Sprintf("%s/entry/%d/bump", srv.URL, pid), url.Values{})
|
||||
jsonBody := body(t, resp)
|
||||
if ct := resp.Header.Get("Content-Type"); !strings.HasPrefix(ct, "application/json") {
|
||||
t.Fatalf("API-Client: erwartet JSON, bekam %q", ct)
|
||||
}
|
||||
if !strings.Contains(jsonBody, `"retry_after"`) {
|
||||
t.Errorf("JSON-Antwort ohne retry_after:\n%s", jsonBody)
|
||||
}
|
||||
|
||||
// Nicht angemeldet: kein Formular im Feed, POST -> 401 als HTML.
|
||||
_, anonPage := getHTML(t, newClient(t), srv.URL+"/feed")
|
||||
if strings.Contains(anonPage, fmt.Sprintf(`action="/entry/%d/bump"`, pid)) {
|
||||
t.Error("repost-formular für nicht angemeldete besucher")
|
||||
}
|
||||
resp = postFormHTML(t, newClient(t), fmt.Sprintf("%s/entry/%d/bump", srv.URL, pid), url.Values{})
|
||||
page = body(t, resp)
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
t.Fatalf("anon repost: erwartet 401, bekam %d", resp.StatusCode)
|
||||
}
|
||||
if ct := resp.Header.Get("Content-Type"); !strings.HasPrefix(ct, "text/html") {
|
||||
t.Fatalf("anon repost: erwartet HTML, bekam %q", ct)
|
||||
}
|
||||
}
|
||||
|
||||
// createReply erstellt eine Antwort auf parentPID und liefert deren pid.
|
||||
func createReply(t *testing.T, c *http.Client, srv *httptest.Server, content string, parentPID int64) int64 {
|
||||
t.Helper()
|
||||
@@ -1212,6 +1291,11 @@ func TestBeitragOhneJavaScript(t *testing.T) {
|
||||
if !strings.Contains(page, `type="file"`) {
|
||||
t.Errorf("kein dateifeld:\n%s", excerpt(page, "compose"))
|
||||
}
|
||||
// Das Formular trägt den Rücksprungort mit, damit ein abgelehnter Post ihn
|
||||
// nicht verliert und die neu gerenderten Knöpfe auf "/feed" zeigen statt "/".
|
||||
if !strings.Contains(page, `name="return_to" value="/feed"`) {
|
||||
t.Errorf("beitragsformular ohne return_to:\n%s", excerpt(page, "compose"))
|
||||
}
|
||||
|
||||
// Nicht angemeldet: kein Formular.
|
||||
_, anonPage := getHTML(t, newClient(t), srv.URL+"/feed")
|
||||
@@ -1252,7 +1336,9 @@ func TestBeitragOhneJavaScript(t *testing.T) {
|
||||
}
|
||||
|
||||
// Leerer Beitrag -> Feed mit Meldung und erhaltenem Text, kein Redirect.
|
||||
resp = postFormHTML(t, alice, srv.URL+"/entry/create", url.Values{"content": {" "}})
|
||||
// return_to wie vom Formular mitgeschickt.
|
||||
resp = postFormHTML(t, alice, srv.URL+"/entry/create",
|
||||
url.Values{"content": {" "}, "return_to": {"/feed"}, "page": {"0"}})
|
||||
failPage := body(t, resp)
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Fatalf("leerer beitrag: erwartet 400, bekam %d", resp.StatusCode)
|
||||
@@ -1264,6 +1350,14 @@ func TestBeitragOhneJavaScript(t *testing.T) {
|
||||
if !strings.Contains(failPage, "mein erster ssr-beitrag") {
|
||||
t.Errorf("feed nach fehler nicht mitgerendert:\n%s", excerpt(failPage, "feed"))
|
||||
}
|
||||
// Die neu gerenderten Interaktions-Knöpfe zeigen zurück in den Feed, nicht
|
||||
// auf "/" (die noch per JS bediente Startseite).
|
||||
if strings.Contains(failPage, `name="return_to" value="/#e`) {
|
||||
t.Errorf("interaktions-knöpfe zeigen nach dem fehler auf \"/\":\n%s", excerpt(failPage, "vote"))
|
||||
}
|
||||
if !strings.Contains(failPage, `name="return_to" value="/feed#e`) {
|
||||
t.Errorf("interaktions-knöpfe ohne feed-rücksprung:\n%s", excerpt(failPage, "vote"))
|
||||
}
|
||||
|
||||
// Getippter Text überlebt einen abgelehnten Upload.
|
||||
resp = postMultipartHTML(t, alice, srv.URL+"/entry/create",
|
||||
@@ -1367,6 +1461,10 @@ func TestThreadOhneJavaScript(t *testing.T) {
|
||||
if !strings.Contains(page, fmt.Sprintf(`name="reply_to" value="%d"`, mitte)) {
|
||||
t.Fatalf("antwortformular ohne reply_to:\n%s", excerpt(page, "compose"))
|
||||
}
|
||||
// Das Antwortformular trägt den Rücksprung in den Thread mit.
|
||||
if !strings.Contains(page, fmt.Sprintf(`name="return_to" value="/beitrag/%d"`, mitte)) {
|
||||
t.Errorf("antwortformular ohne return_to:\n%s", excerpt(page, "compose"))
|
||||
}
|
||||
|
||||
// Antworten ohne JS -> 303 in den Thread des Elternbeitrags.
|
||||
resp := postFormHTML(t, alice, srv.URL+"/entry/create",
|
||||
@@ -1390,9 +1488,11 @@ func TestThreadOhneJavaScript(t *testing.T) {
|
||||
t.Errorf("anker %q aus dem redirect existiert nicht in der seite", anchor)
|
||||
}
|
||||
|
||||
// Fehlerfall: leere Antwort -> Thread neu, nicht der Feed.
|
||||
// Fehlerfall: leere Antwort -> Thread neu, nicht der Feed. return_to wie
|
||||
// vom Formular mitgeschickt.
|
||||
resp = postFormHTML(t, alice, srv.URL+"/entry/create",
|
||||
url.Values{"content": {" "}, "reply_to": {fmt.Sprint(mitte)}})
|
||||
url.Values{"content": {" "}, "reply_to": {fmt.Sprint(mitte)},
|
||||
"return_to": {fmt.Sprintf("/beitrag/%d", mitte)}})
|
||||
failPage := body(t, resp)
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Fatalf("leere antwort: erwartet 400, bekam %d", resp.StatusCode)
|
||||
@@ -1403,6 +1503,14 @@ func TestThreadOhneJavaScript(t *testing.T) {
|
||||
if !strings.Contains(failPage, "mittlere antwort") {
|
||||
t.Errorf("thread nach fehler nicht mitgerendert:\n%s", excerpt(failPage, "focus"))
|
||||
}
|
||||
// Die neu gerenderten Interaktions-Knöpfe zeigen zurück in den Thread,
|
||||
// nicht auf "/".
|
||||
if strings.Contains(failPage, `name="return_to" value="/#e`) {
|
||||
t.Errorf("interaktions-knöpfe zeigen nach dem fehler auf \"/\":\n%s", excerpt(failPage, "vote"))
|
||||
}
|
||||
if !strings.Contains(failPage, fmt.Sprintf(`name="return_to" value="/beitrag/%d#e`, mitte)) {
|
||||
t.Errorf("interaktions-knöpfe ohne thread-rücksprung:\n%s", excerpt(failPage, "vote"))
|
||||
}
|
||||
|
||||
// Unbekannter Beitrag -> 404, keine leere Seite.
|
||||
if status, _ := getHTML(t, anon, srv.URL+"/beitrag/999999"); status != http.StatusNotFound {
|
||||
|
||||
@@ -496,11 +496,20 @@ func handleBump(w http.ResponseWriter, r *http.Request) error {
|
||||
if remaining < 0 {
|
||||
remaining = 0
|
||||
}
|
||||
// Bewusst kein Taxonomie-Fehler: die Antwort trägt Nutzdaten
|
||||
// (retry_after, bump_count), die das Frontend zum Sperren des Buttons
|
||||
// braucht -- ein reiner Fehlerkörper kann die nicht transportieren.
|
||||
// Der Cooldown ist auch kein Fehlerfall, sondern ein regulärer
|
||||
// Ausgang des Bumpens.
|
||||
// Ohne JavaScript: der Cooldown ist eine eigene Fehlerseite mit der
|
||||
// Restzeit -- ein stiller Rücksprung wäre von "hat geklappt" nicht zu
|
||||
// unterscheiden.
|
||||
if !wantsJSON(r) {
|
||||
hours := (remaining + 3599) / 3600
|
||||
if hours < 1 {
|
||||
hours = 1
|
||||
}
|
||||
return RateLimited("entry.bump_cooldown", fmt.Sprintf(
|
||||
"Dieser Beitrag wurde erst kürzlich repostet. Nächster Repost in etwa %d Stunde(n).", hours))
|
||||
}
|
||||
// JSON-Zweig: bewusst kein Taxonomie-Fehler -- die Antwort trägt
|
||||
// Nutzdaten (retry_after, bump_count), die das Frontend zum Sperren des
|
||||
// Buttons braucht, und der Cooldown ist ein regulärer Ausgang.
|
||||
w.Header().Set("Retry-After", strconv.FormatInt(remaining, 10))
|
||||
writeJSON(w, http.StatusTooManyRequests, map[string]any{
|
||||
"error": apiErrorBody{
|
||||
@@ -512,6 +521,13 @@ func handleBump(w http.ResponseWriter, r *http.Request) error {
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ohne JavaScript zurück an den Beitrag, auf dem repostet wurde -- der
|
||||
// Anker in return_to hält die Position.
|
||||
if !wantsJSON(r) {
|
||||
redirectBack(w, r, threadAnchor(pid))
|
||||
return nil
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"pid": pid,
|
||||
"bump_count": bumpCount,
|
||||
|
||||
+41
-1
@@ -80,6 +80,31 @@ type entryView struct {
|
||||
DeleteURL string
|
||||
}
|
||||
|
||||
// bumpRemaining ist die Restsperre des Reposts in Sekunden (0 = wieder
|
||||
// möglich). Spiegelt die Bedingung im UPDATE von handleBump; die Vorlage
|
||||
// braucht den Wert, um den Repost-Knopf zu sperren und die Wartezeit
|
||||
// anzuzeigen -- das Gegenstück zu setupBump in web/js/feed.js.
|
||||
func (v entryView) bumpRemaining() int64 {
|
||||
rem := v.LastBump + v.BumpCount*bumpStep - time.Now().Unix()
|
||||
if rem < 0 {
|
||||
return 0
|
||||
}
|
||||
return rem
|
||||
}
|
||||
|
||||
// BumpLocked meldet, ob der Repost dieses Beitrags gerade im Cooldown steckt.
|
||||
func (v entryView) BumpLocked() bool { return v.bumpRemaining() > 0 }
|
||||
|
||||
// BumpWaitHours ist die Restsperre auf volle Stunden aufgerundet (mindestens
|
||||
// 1, solange überhaupt Zeit übrig ist) -- wie die Anzeige im JS-Frontend.
|
||||
func (v entryView) BumpWaitHours() int64 {
|
||||
h := (v.bumpRemaining() + 3599) / 3600
|
||||
if h < 1 {
|
||||
return 1
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// composeForm hält die Eingaben des Beitragsformulars über einen
|
||||
// fehlgeschlagenen Versuch hinweg. Die angehängte Datei fehlt dabei
|
||||
// zwangsläufig: ein <input type="file"> lässt sich aus Sicherheitsgründen
|
||||
@@ -97,6 +122,13 @@ type feedView struct {
|
||||
PrevURL string
|
||||
NextURL string
|
||||
|
||||
// ReturnTo ist der Rücksprungort dieser Seite (beim GET die Seite selbst).
|
||||
// Das Beitragsformular trägt ihn als verstecktes Feld, damit er einen
|
||||
// abgelehnten Post übersteht -- sonst fiele das neu gerenderte Formular
|
||||
// samt seiner Interaktions-Knöpfe auf den Default "/" zurück (die noch per
|
||||
// JS bediente Startseite) statt auf "/feed".
|
||||
ReturnTo string
|
||||
|
||||
// CanPost steuert, ob das Beitragsformular erscheint. Ohne Anmeldung gibt
|
||||
// es nichts zu schreiben; über TOR wird das Schreiben abgelehnt (blockTOR),
|
||||
// und ein Formular, das beim Absenden immer scheitert, wäre schlechter als
|
||||
@@ -178,11 +210,13 @@ func buildFeedView(r *http.Request, uid int64, rootsOnly bool) (feedView, error)
|
||||
return feedView{}, Internal(err)
|
||||
}
|
||||
|
||||
back := returnTo(r)
|
||||
v := feedView{
|
||||
Entries: toEntryViews(entries, tallies, returnTo(r), viewer),
|
||||
Entries: toEntryViews(entries, tallies, back, viewer),
|
||||
Page: page,
|
||||
Pages: pages,
|
||||
IsTOR: isTOR(r),
|
||||
ReturnTo: back,
|
||||
}
|
||||
v.CanPost = viewer != 0 && !v.IsTOR
|
||||
if page > 0 {
|
||||
@@ -203,6 +237,11 @@ type threadView struct {
|
||||
CanPost bool
|
||||
IsTOR bool
|
||||
Compose composeForm
|
||||
|
||||
// ReturnTo wie bei feedView: das Antwortformular trägt ihn versteckt mit,
|
||||
// damit eine abgelehnte Antwort die Interaktions-Knöpfe nicht auf "/"
|
||||
// zurückwirft statt auf den Thread.
|
||||
ReturnTo string
|
||||
}
|
||||
|
||||
// toEntryViews reichert Beiträge mit Abstimmungsstand, Rücksprungziel und
|
||||
@@ -272,6 +311,7 @@ func buildThreadView(r *http.Request, pid int64) (threadView, error) {
|
||||
Entry: toEntryViews([]Entry{t.Entry}, tallies, back, viewer)[0],
|
||||
Replies: toEntryViews(t.Replies, tallies, back, viewer),
|
||||
IsTOR: isTOR(r),
|
||||
ReturnTo: back,
|
||||
}
|
||||
// Auf einen gelöschten Beitrag lässt sich nicht antworten -- er hat keinen
|
||||
// Autor mehr und der Platzhalter soll den Thread nur zusammenhalten.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 9.0 KiB |
@@ -51,6 +51,14 @@
|
||||
<button class="ghost{{if eq .Selected "right"}} selected{{end}}"
|
||||
name="mode" value="right">Rechts</button>
|
||||
</form>
|
||||
{{/* Repost hebt den Beitrag im Feed wieder nach oben. Der Cooldown
|
||||
sitzt am Beitrag (global) und wächst pro Repost; ist er aktiv,
|
||||
ist der Knopf gesperrt und die Restzeit steht daneben. */}}
|
||||
<form method="post" action="/entry/{{.PID}}/bump">
|
||||
<input type="hidden" name="return_to" value="{{.ReturnTo}}#e{{.PID}}">
|
||||
<button class="bump" type="submit"{{if .BumpLocked}} disabled{{end}}>Repost ({{.BumpCount}})</button>
|
||||
{{if .BumpLocked}}<span class="muted"> · in {{.BumpWaitHours}}h</span>{{end}}
|
||||
</form>
|
||||
{{else}}
|
||||
{{/* Ohne Anmeldung nur der Stand -- ein Formular hier würde
|
||||
ausschließlich 401 liefern. */}}
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
statt der Datei -- ohne JavaScript gibt es keinen zweiten Versuch. */}}
|
||||
<form method="post" action="/entry/create" enctype="multipart/form-data">
|
||||
<input type="hidden" name="page" value="{{.Page}}">
|
||||
{{/* return_to überlebt einen abgelehnten Post: sonst rendert failCompose
|
||||
den Feed mit Interaktions-Knöpfen, die auf "/" statt "/feed" zeigen. */}}
|
||||
<input type="hidden" name="return_to" value="{{.ReturnTo}}">
|
||||
{{with $.Err}}<p class="msg">{{.Msg}}</p>{{end}}
|
||||
<label for="content">Neuer Beitrag</label>
|
||||
<textarea id="content" name="content" rows="4" maxlength="1000"
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
<section id="compose">
|
||||
<form method="post" action="/entry/create" enctype="multipart/form-data">
|
||||
<input type="hidden" name="reply_to" value="{{.Entry.PID}}">
|
||||
{{/* return_to überlebt eine abgelehnte Antwort: sonst zeigen die neu
|
||||
gerenderten Interaktions-Knöpfe auf "/" statt auf den Thread. */}}
|
||||
<input type="hidden" name="return_to" value="{{.ReturnTo}}">
|
||||
{{with $.Err}}<p class="msg">{{.Msg}}</p>{{end}}
|
||||
<label for="content">Antworten</label>
|
||||
<textarea id="content" name="content" rows="4" maxlength="1000"
|
||||
|
||||
+1
-2
@@ -78,8 +78,7 @@ registerForm.addEventListener("submit", async (e) => {
|
||||
const feed = createFeed(
|
||||
document.getElementById("feed"),
|
||||
document.getElementById("sentinel"),
|
||||
(page) => `/entry/feed/${page}`,
|
||||
{ adEvery: 5 }
|
||||
(page) => `/entry/feed/${page}`
|
||||
);
|
||||
|
||||
// --- Neuer Beitrag ---
|
||||
|
||||
+3
-31
@@ -261,35 +261,12 @@ function setupReport(el, pid) {
|
||||
});
|
||||
}
|
||||
|
||||
// renderAd baut einen In-Feed-Werbeslot: das Banner (selbst gehostet, sonst
|
||||
// würde die CSP default-src 'self' es blocken), darunter der Pflicht-Hinweis
|
||||
// "Werbung" und der Link zur Eigenvermarktung. rel="sponsored nofollow" macht
|
||||
// den bezahlten Charakter für Suchmaschinen kenntlich.
|
||||
function renderAd() {
|
||||
const el = document.createElement("aside");
|
||||
el.className = "ad";
|
||||
el.innerHTML = `
|
||||
<a class="ad-banner" href="/werben" rel="sponsored nofollow noopener">
|
||||
<img src="/static/ads/placeholder.png" alt="Werbung" loading="lazy">
|
||||
</a>
|
||||
<div class="ad-meta muted">
|
||||
<span>Werbung</span>
|
||||
<a href="/werben">Auf dem Kontrollverlust werben</a>
|
||||
</div>
|
||||
`;
|
||||
return el;
|
||||
}
|
||||
|
||||
// createFeed hängt Infinite-Scroll an feedEl/sentinelEl und lädt Seiten über
|
||||
// urlFor(page). opts.adEvery (>0) blendet nach je N Beiträgen einen
|
||||
// Werbebanner ein — nur im Top-Level-Feed, nicht in User-/Thread-Feeds.
|
||||
// Gibt { reset } zurück, um den Feed neu zu laden.
|
||||
function createFeed(feedEl, sentinelEl, urlFor, opts = {}) {
|
||||
const adEvery = opts.adEvery || 0;
|
||||
// urlFor(page). Gibt { reset } zurück, um den Feed neu zu laden.
|
||||
function createFeed(feedEl, sentinelEl, urlFor) {
|
||||
let page = 0;
|
||||
let loading = false;
|
||||
let done = false;
|
||||
let shown = 0; // fortlaufende Beitragszahl über Seitengrenzen hinweg
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || done) return;
|
||||
@@ -303,11 +280,7 @@ function createFeed(feedEl, sentinelEl, urlFor, opts = {}) {
|
||||
end.textContent = "YOU REACHED THE END!";
|
||||
feedEl.appendChild(end);
|
||||
} else {
|
||||
entries.forEach((e) => {
|
||||
feedEl.appendChild(renderEntry(e));
|
||||
shown++;
|
||||
if (adEvery && shown % adEvery === 0) feedEl.appendChild(renderAd());
|
||||
});
|
||||
entries.forEach((e) => feedEl.appendChild(renderEntry(e)));
|
||||
page++;
|
||||
}
|
||||
loading = false;
|
||||
@@ -326,7 +299,6 @@ function createFeed(feedEl, sentinelEl, urlFor, opts = {}) {
|
||||
function reset() {
|
||||
page = 0;
|
||||
done = false;
|
||||
shown = 0;
|
||||
feedEl.innerHTML = "";
|
||||
loadMore();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user