forked from PrivateCoffee/wikimore
Compare commits
13 commits
blitzwing-
...
main
Author | SHA1 | Date | |
---|---|---|---|
b0b3c3d3db | |||
de15cbdb75 | |||
4479a109b3 | |||
c702671243 | |||
2167528f19 | |||
5bd90742b0 | |||
5d35208968 | |||
cfc88a5c4e | |||
01b63f7a82 | |||
7715094173 | |||
28852e812c | |||
d9a395ec76 | |||
a4e9eb37ad |
5 changed files with 389 additions and 14 deletions
27
README.md
27
README.md
|
@ -20,12 +20,29 @@ This project is still in development and more features will be added in the futu
|
||||||
## Instances
|
## Instances
|
||||||
|
|
||||||
| URL | Provided by | Country | Comments |
|
| URL | Provided by | Country | Comments |
|
||||||
| ----------------------------------------------------------- | ----------------------------------------- | ------- | -------- |
|
| ----------------------------------------------------------------- | ----------------------------------------------- | ------------- | -------- |
|
||||||
| [wikimore.private.coffee](https://wikimore.private.coffee/) | [Private.coffee](https://private.coffee/) | Austria | |
|
| [wikimore.private.coffee](https://wikimore.private.coffee/) | [Private.coffee](https://private.coffee/) | Austria 🇦🇹 🇪🇺 | |
|
||||||
| [wm.bloat.cat](https://wm.bloat.cat/) | [bloat.cat](https://bloat.cat/) | Germany | |
|
| [wm.bloat.cat](https://wm.bloat.cat/) | [bloat.cat](https://bloat.cat/) | Germany 🇩🇪 🇪🇺 | |
|
||||||
| [wikimore.blitzw.in](https://wikimore.blitzw.in/) | [blitzw.in](https://blitzw.in/) | Denmark | |
|
| [wikimore.blitzw.in](https://wikimore.blitzw.in/) | [blitzw.in](https://blitzw.in/) | Denmark 🇩🇰 🇪🇺 | |
|
||||||
|
| [wikimore.lumaeris.com](https://wikimore.lumaeris.com/) | [Lumaeris](https://lumaeris.com/) | Germany 🇩🇪 🇪🇺 | |
|
||||||
|
| [wikimore.darkness.services](https://wikimore.darkness.services/) | [Darkness.services](https://darkness.services/) | USA 🇺🇸 | |
|
||||||
|
|
||||||
If you operate a public instance of Wikimore and would like to have it listed here, please open an issue or a pull request.
|
### Tor Hidden Services
|
||||||
|
|
||||||
|
| URL | Provided by | Country | Comments |
|
||||||
|
| ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | ------------- | -------- |
|
||||||
|
| [wikimore.coffee2m3bjsrrqqycx6ghkxrnejl2q6nl7pjw2j4clchjj6uk5zozad.onion](http://wikimore.coffee2m3bjsrrqqycx6ghkxrnejl2q6nl7pjw2j4clchjj6uk5zozad.onion/) | [Private.coffee](https://private.coffee/) | Austria 🇦🇹 🇪🇺 | |
|
||||||
|
| [wikimore.darknessrdor43qkl2ngwitj72zdavfz2cead4t5ed72bybgauww5lyd.onion](http://wikimore.darknessrdor43qkl2ngwitj72zdavfz2cead4t5ed72bybgauww5lyd.onion/) | [Darkness.services](https://darkness.services/) | USA 🇺🇸 | |
|
||||||
|
|
||||||
|
### Adding Your Instance
|
||||||
|
|
||||||
|
To add your own instance to this list, please open a pull request or issue, see below.
|
||||||
|
|
||||||
|
## Opening Issues
|
||||||
|
|
||||||
|
If you're having problems using Wikimore, or if you have ideas or feedback for us, feel free to open an issue in the [Private.coffee Git](https://git.private.coffee/PrivateCoffee/wikimore/issues) or on [Github](https://github.com/PrivateCoffee/wikimore/issues).
|
||||||
|
|
||||||
|
Of course, you can also join our [Matrix room](https://matrix.pcof.fi/#/#wikimore:private.coffee) to discuss your ideas with us.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "wikimore"
|
name = "wikimore"
|
||||||
version = "0.1.5"
|
version = "0.1.8"
|
||||||
authors = [{ name = "Private.coffee Team", email = "support@private.coffee" }]
|
authors = [{ name = "Private.coffee Team", email = "support@private.coffee" }]
|
||||||
description = "A simple frontend for Wikimedia wikis"
|
description = "A simple frontend for Wikimedia wikis"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
|
@ -123,6 +123,96 @@ logger.debug(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Get number of active Wikipedia users for each language
|
||||||
|
def get_active_users() -> Dict[str, int]:
|
||||||
|
"""Fetch the number of active Wikipedia users for each language.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, int]: A dictionary mapping language codes to the number of active Wikipedia users.
|
||||||
|
"""
|
||||||
|
path = "/w/api.php?action=query&format=json&meta=siteinfo&siprop=statistics"
|
||||||
|
|
||||||
|
active_users = {}
|
||||||
|
|
||||||
|
for lang, data in app.languages.items():
|
||||||
|
try:
|
||||||
|
url = f"{data['projects']['wiki']}{path}"
|
||||||
|
with urllib.request.urlopen(url) as response:
|
||||||
|
data = json.loads(response.read().decode())
|
||||||
|
active_users[lang] = data["query"]["statistics"]["activeusers"]
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error fetching active users for {lang}: {e}")
|
||||||
|
|
||||||
|
return sorted(active_users.items(), key=lambda x: x[1], reverse=True)
|
||||||
|
|
||||||
|
|
||||||
|
if os.environ.get("NO_LANGSORT", False):
|
||||||
|
LANGSORT = []
|
||||||
|
elif os.environ.get("LANGSORT") == "auto":
|
||||||
|
LANGSORT = [lang for lang, _ in get_active_users()[:50]]
|
||||||
|
elif os.environ.get("LANGSORT"):
|
||||||
|
LANGSORT = os.environ["LANGSORT"].split(",")
|
||||||
|
else:
|
||||||
|
# Opinionated sorting of languages
|
||||||
|
LANGSORT = [
|
||||||
|
"en",
|
||||||
|
"es",
|
||||||
|
"ja",
|
||||||
|
"de",
|
||||||
|
"fr",
|
||||||
|
"zh",
|
||||||
|
"ru",
|
||||||
|
"it",
|
||||||
|
"pt",
|
||||||
|
"pl",
|
||||||
|
"nl",
|
||||||
|
"ar",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def langsort(input: list[dict], key: str = "lang") -> list[dict]:
|
||||||
|
"""Sorting of language data.
|
||||||
|
|
||||||
|
Sorts a list of dictionaries containing "lang" keys such that the most common languages are first.
|
||||||
|
|
||||||
|
Allows specifying a custom order using the `LANGSORT` environment variable.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input (list[dict]): A list of dictionaries containing "lang" keys.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[dict]: The sorted list of dictionaries.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not LANGSORT:
|
||||||
|
return input
|
||||||
|
|
||||||
|
output = []
|
||||||
|
|
||||||
|
for lang in LANGSORT:
|
||||||
|
for item in input:
|
||||||
|
if item[key] == lang:
|
||||||
|
output.append(item)
|
||||||
|
|
||||||
|
for item in input:
|
||||||
|
if item[key] not in LANGSORT:
|
||||||
|
output.append(item)
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
logger.debug("Initialized language sort order")
|
||||||
|
|
||||||
|
app_languages = [
|
||||||
|
{"lang": lang, "name": data["name"]} for lang, data in app.languages.items()
|
||||||
|
]
|
||||||
|
app_languages = langsort(app_languages)
|
||||||
|
|
||||||
|
app.languages = {
|
||||||
|
lang: app.languages[lang] for lang in [lang["lang"] for lang in app_languages]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def render_template(*args, **kwargs) -> Text:
|
def render_template(*args, **kwargs) -> Text:
|
||||||
"""A wrapper around Flask's `render_template` that adds the `languages` and `wikimedia_projects` context variables.
|
"""A wrapper around Flask's `render_template` that adds the `languages` and `wikimedia_projects` context variables.
|
||||||
|
|
||||||
|
@ -243,18 +333,16 @@ def inbound_redirect(domain: str, url: str) -> Union[Text, Response, Tuple[Text,
|
||||||
Returns:
|
Returns:
|
||||||
Response: A redirect to the corresponding route
|
Response: A redirect to the corresponding route
|
||||||
"""
|
"""
|
||||||
|
# TODO: Make this the default route scheme instead of a redirect
|
||||||
|
|
||||||
for language, language_projects in app.languages.items():
|
for language, language_projects in app.languages.items():
|
||||||
for project_name, project_url in language_projects["projects"].items():
|
for project_name, project_url in language_projects["projects"].items():
|
||||||
if project_url == f"https://{domain}":
|
if project_url == f"https://{domain}":
|
||||||
return redirect(
|
return redirect(f"{url_for('home')}{project_name}/{language}/{url}")
|
||||||
f"{url_for('home')}{project_name}/{language}/{url}"
|
|
||||||
)
|
|
||||||
|
|
||||||
for project_name, project_url in app.languages["special"]["projects"].items():
|
for project_name, project_url in app.languages["special"]["projects"].items():
|
||||||
if project_url == f"https://{domain}":
|
if project_url == f"https://{domain}":
|
||||||
return redirect(
|
return redirect(f"{url_for('home')}/{project_name}/{language}/{url}")
|
||||||
f"{url_for('home')}/{project_name}/{language}/{url}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# TODO / IDEA: Handle non-Wikimedia Mediawiki projects here?
|
# TODO / IDEA: Handle non-Wikimedia Mediawiki projects here?
|
||||||
|
|
||||||
|
@ -267,6 +355,7 @@ def inbound_redirect(domain: str, url: str) -> Union[Text, Response, Tuple[Text,
|
||||||
404,
|
404,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/<project>/<lang>/wiki/<path:title>")
|
@app.route("/<project>/<lang>/wiki/<path:title>")
|
||||||
def wiki_article(
|
def wiki_article(
|
||||||
project: str, lang: str, title: str
|
project: str, lang: str, title: str
|
||||||
|
@ -303,11 +392,146 @@ def wiki_article(
|
||||||
|
|
||||||
logger.debug(f"Fetching {title} from {base_url}")
|
logger.debug(f"Fetching {title} from {base_url}")
|
||||||
|
|
||||||
|
# Check if the article is something we need to handle differently
|
||||||
|
info_api_request = urllib.request.Request(
|
||||||
|
f"{base_url}/w/api.php?action=query&format=json&titles={escape(quote(title.replace(' ', '_')), True)}&prop=info|pageprops|categoryinfo|langlinks&lllimit=500",
|
||||||
|
headers=HEADERS,
|
||||||
|
)
|
||||||
|
|
||||||
|
category_members = []
|
||||||
|
interwiki = []
|
||||||
|
badges = []
|
||||||
|
|
||||||
|
with urllib.request.urlopen(info_api_request) as response:
|
||||||
|
logger.debug(
|
||||||
|
f"Tried to fetch info for {title} from {info_api_request.full_url}"
|
||||||
|
)
|
||||||
|
data = json.loads(response.read().decode())
|
||||||
|
page = data["query"]["pages"].popitem()[1]
|
||||||
|
|
||||||
|
langlinks = page.get("langlinks", [])
|
||||||
|
|
||||||
|
logger.debug(f"Original Interwiki links for {title}: {langlinks}")
|
||||||
|
|
||||||
|
# Get interwiki links and translate them to internal links where possible
|
||||||
|
for link in langlinks:
|
||||||
|
try:
|
||||||
|
interwiki_lang = link["lang"]
|
||||||
|
interwiki_title = link["*"]
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
f"Generating interwiki link for: {interwiki_lang}.{project}/{interwiki_title}"
|
||||||
|
)
|
||||||
|
|
||||||
|
interwiki_url = url_for(
|
||||||
|
"wiki_article",
|
||||||
|
project=project,
|
||||||
|
lang=interwiki_lang,
|
||||||
|
title=interwiki_title,
|
||||||
|
)
|
||||||
|
link["url"] = interwiki_url
|
||||||
|
|
||||||
|
link["langname"] = app.languages[interwiki_lang]["name"]
|
||||||
|
|
||||||
|
interwiki.append(link)
|
||||||
|
|
||||||
|
except KeyError as e:
|
||||||
|
logger.error(
|
||||||
|
f"Error processing interwiki link for title {title} in language {lang}: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get badges (e.g. "Good Article", "Featured Article")
|
||||||
|
props = page.get("pageprops", {})
|
||||||
|
|
||||||
|
for prop in props:
|
||||||
|
if prop.startswith("wikibase-badge-"):
|
||||||
|
try:
|
||||||
|
badge_id = prop.replace("wikibase-badge-", "")
|
||||||
|
|
||||||
|
# Fetch the badge data from Wikidata
|
||||||
|
badge_request = urllib.request.Request(
|
||||||
|
f"https://www.wikidata.org/w/api.php?action=wbgetentities&format=json&ids={badge_id}&languages={lang}",
|
||||||
|
headers=HEADERS,
|
||||||
|
)
|
||||||
|
|
||||||
|
with urllib.request.urlopen(badge_request) as badge_response:
|
||||||
|
logger.debug(
|
||||||
|
f"Tried to fetch badge {badge_id} from {badge_request.full_url}"
|
||||||
|
)
|
||||||
|
|
||||||
|
badge_data = json.loads(badge_response.read().decode())
|
||||||
|
|
||||||
|
badge = badge_data["entities"][badge_id]["labels"][lang][
|
||||||
|
"value"
|
||||||
|
]
|
||||||
|
badge_image = badge_data["entities"][badge_id]["claims"]["P18"][
|
||||||
|
0
|
||||||
|
]["mainsnak"]["datavalue"]["value"]
|
||||||
|
badges.append(
|
||||||
|
{
|
||||||
|
"title": badge,
|
||||||
|
"url": f"https://www.wikidata.org/wiki/{badge_id}",
|
||||||
|
"image": get_proxy_url(
|
||||||
|
f"https://commons.wikimedia.org/wiki/Special:Redirect/file/{badge_image}"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error fetching badge {prop}: {e}")
|
||||||
|
|
||||||
|
# If the article is a category, fetch the category members
|
||||||
|
if "categoryinfo" in page:
|
||||||
|
category_api_url = f"{base_url}/w/api.php?action=query&format=json&list=categorymembers&cmtitle={escape(quote(title.replace(' ', '_')), True)}&cmlimit=500"
|
||||||
|
|
||||||
|
category_api_request = urllib.request.Request(
|
||||||
|
category_api_url,
|
||||||
|
headers=HEADERS,
|
||||||
|
)
|
||||||
|
|
||||||
|
all_members = []
|
||||||
|
|
||||||
|
with urllib.request.urlopen(category_api_request) as category_api_response:
|
||||||
|
logger.debug(
|
||||||
|
f"Tried to fetch category members for {title} from {category_api_request.full_url}"
|
||||||
|
)
|
||||||
|
data = json.loads(category_api_response.read().decode())
|
||||||
|
category_members = data["query"]["categorymembers"]
|
||||||
|
all_members += category_members
|
||||||
|
|
||||||
|
if "continue" in data:
|
||||||
|
continue_params = f"&cmcontinue={data['continue']['cmcontinue']}"
|
||||||
|
category_api_request = urllib.request.Request(
|
||||||
|
category_api_url + continue_params,
|
||||||
|
headers=HEADERS,
|
||||||
|
)
|
||||||
|
|
||||||
|
with urllib.request.urlopen(
|
||||||
|
category_api_request
|
||||||
|
) as category_api_response:
|
||||||
|
data = json.loads(category_api_response.read().decode())
|
||||||
|
all_members += data["query"]["categorymembers"]
|
||||||
|
|
||||||
|
category_members = all_members
|
||||||
|
|
||||||
|
for member in category_members:
|
||||||
|
member["url"] = url_for(
|
||||||
|
"wiki_article",
|
||||||
|
project=project,
|
||||||
|
lang=lang,
|
||||||
|
title=member["title"],
|
||||||
|
)
|
||||||
|
|
||||||
|
interwiki = langsort(interwiki)
|
||||||
|
|
||||||
|
# Prepare the API request to fetch the article content
|
||||||
api_request = urllib.request.Request(
|
api_request = urllib.request.Request(
|
||||||
f"{base_url}/api/rest_v1/page/html/{escape(quote(title.replace(' ', '_')), True).replace('/', '%2F')}",
|
f"{base_url}/api/rest_v1/page/html/{escape(quote(title.replace(' ', '_')), True).replace('/', '%2F')}",
|
||||||
headers=HEADERS,
|
headers=HEADERS,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger.debug(f"Article content URL: {api_request.full_url}")
|
||||||
|
|
||||||
# Add the `variant` header if the `variant` query parameter is present
|
# Add the `variant` header if the `variant` query parameter is present
|
||||||
# This is used to fetch articles in a specific script variant (https://www.mediawiki.org/wiki/Writing_systems/LanguageConverter)
|
# This is used to fetch articles in a specific script variant (https://www.mediawiki.org/wiki/Writing_systems/LanguageConverter)
|
||||||
if request.args.get("variant", None):
|
if request.args.get("variant", None):
|
||||||
|
@ -428,6 +652,9 @@ def wiki_article(
|
||||||
for img in soup.find_all("img"):
|
for img in soup.find_all("img"):
|
||||||
img["src"] = get_proxy_url(img["src"])
|
img["src"] = get_proxy_url(img["src"])
|
||||||
|
|
||||||
|
# While we're at it, ensure that images are loaded lazily
|
||||||
|
img["loading"] = "lazy"
|
||||||
|
|
||||||
for source in soup.find_all("source"):
|
for source in soup.find_all("source"):
|
||||||
source["src"] = get_proxy_url(source["src"])
|
source["src"] = get_proxy_url(source["src"])
|
||||||
|
|
||||||
|
@ -487,6 +714,9 @@ def wiki_article(
|
||||||
project=project,
|
project=project,
|
||||||
rtl=rtl,
|
rtl=rtl,
|
||||||
license=license,
|
license=license,
|
||||||
|
interwiki=interwiki,
|
||||||
|
badges=badges,
|
||||||
|
category_members=category_members,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1229,3 +1229,94 @@ Currently blocked by implementation of comments retrieval in the backend
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Language selector styling */
|
||||||
|
.language-selector {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-selector-toggle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-selector-label {
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.5em;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-selector-label-text {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-selector-label-icon {
|
||||||
|
display: inline-block;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 0.25em solid transparent;
|
||||||
|
border-right: 0.25em solid transparent;
|
||||||
|
border-top: 0.25em solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-selector-menu {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-selector-toggle:checked + .language-selector-label + .language-selector-menu {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-selector-list {
|
||||||
|
list-style-type: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-selector-item {
|
||||||
|
border-top: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-selector-link {
|
||||||
|
display: block;
|
||||||
|
padding: 0.5em;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-selector-link:hover {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-selector-link:active {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-selector-link:focus {
|
||||||
|
outline: 1px dotted #333;
|
||||||
|
outline: 5px auto -webkit-focus-ring-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-selector-link:active,
|
||||||
|
.language-selector-link:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-selector-link:active,
|
||||||
|
.language-selector-link:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge > img {
|
||||||
|
max-height: 1em;
|
||||||
|
}
|
|
@ -1,8 +1,45 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1 class="title{% if rtl %} title-rtl{% endif %}">{{ title }}</h1>
|
<h1 class="title{% if rtl %} title-rtl{% endif %}">
|
||||||
|
{{ title }}
|
||||||
|
{% if badges %}
|
||||||
|
<span class="badges">
|
||||||
|
{% for badge in badges %}
|
||||||
|
<a href="{{ badge.url }}" class="badge" title="{{ badge.title }}">
|
||||||
|
<img src="{{ badge.image }}" alt="{{ badge.title }}">
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</h1>
|
||||||
|
{% if interwiki %}
|
||||||
|
<div class="language-selector">
|
||||||
|
<input type="checkbox" id="language-selector-toggle" class="language-selector-toggle">
|
||||||
|
<label for="language-selector-toggle" class="language-selector-label">
|
||||||
|
<span class="language-selector-label-text">Language</span>
|
||||||
|
<span class="language-selector-label-icon"></span>
|
||||||
|
</label>
|
||||||
|
<div class="language-selector-menu">
|
||||||
|
<ul class="language-selector-list">
|
||||||
|
{% for lang in interwiki %}
|
||||||
|
<li class="language-selector-item">
|
||||||
|
<a href="{{ lang.url }}" class="language-selector-link">{{ lang.langname }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{{ content|safe }}
|
{{ content|safe }}
|
||||||
|
{% if category_members %}
|
||||||
|
<h2>Pages in category "{{ title }}"</h2>
|
||||||
|
<ul>
|
||||||
|
{% for member in category_members %}
|
||||||
|
<li><a href="{{ member.url }}">{{ member.title }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block license %}
|
{% block license %}
|
||||||
|
|
Loading…
Reference in a new issue