I'm trying to implement a Webpack environment on my Craft 3 projects. In order to dynamically call my hashed resources I'm outputting the in a manifest.json file and importing it into my template.
manifest.json
{"app":["js/app3bfb132e4187389fccd4.js","css/app53079ca4a05210b4af0c.css"],"vendor":"js/vendor49171fe3f01c19da9296.js"}
index.twig
{% set manifest %}
{% include './manifest.json' %}
{% endset %}
The output of that variable is a string. Is there anyway to encode it so that the variables are accessible/printable using only Twig? (Using {{ manifest.app }} for example)
You have to decode the JSON first. I would suggest one of the two approaches:
Create custom manifest function for Twig which will return decoded manifest object
or create json_decode filter for Twig, decode included json content and use it
manifest function
<?php
namespace App\Twig;
class ManifestExtension extends \Twig_Extension
{
private $manifestFile;
public function __construct($manifestFile)
{
$this->manifestFile = $manifestFile;
}
public function getFunctions()
{
return array(
new \Twig_SimpleFunction('manifest', array($this, 'manifest')),
);
}
public function manifest()
{
$content = file_get_contents($this->manifestFile);
return json_decode($content);
}
}
You can register it as a service in services.yml providing path to the manifest.json file.
App\Twig\ManifestExtension:
class: 'App\Twig\ManifestExtension'
arguments: ['%kernel.project_dir%/../public/manifest.json']
tags: [twig.extension]
Usage:
{% set manifest = manifest() %}
json_decode filter
It's already been covered here:
Decoding JSON in Twig
Usage:
{% set manifest %}
{% include './manifest.json' %}
{% endset %}
{% set manifest = manifest | json_decode %}
Related
To create one webpage with multiple Altair charts, all nicely formatted and easily deployable to GitHub Pages, I am trying to get several tools to work together. I am rendering the charts' json into a markdown document with jinja and then using Hugo to convert the markdown to HTML. I am using a Hugo partial (in extend_head.html, shown below) to include the scripts for Vega, Vega-Lite, and VegaEmbed, and I see them in the HTML of the webpage.
extend_head.html
{{- /* Head custom content area start */ -}}
{{- /* Insert any custom code (web-analytics, resources, etc.) - it will appear in the <head></head> section of every page. */ -}}
{{- /* Can be overwritten by partial with the same name in the global layouts. */ -}}
{{- /* Head custom content area end */ -}}
{{- /* To render LaTex math */ -}}
{{ if or .Params.math .Site.Params.math }}
{{ partial "math.html" . }}
{{ end }}
<script src="https://cdn.jsdelivr.net/npm/vega#5"></script>
<script src="https://cdn.jsdelivr.net/npm/vega-lite#4.8.1"></script>
<script src="https://cdn.jsdelivr.net/npm/vega-embed#6"></script>
I also have the following Hugo shortcode to call vegaEmbed. It includes the .Get method with a positional argument.
altairshortcode.html
<p>
<div id="vis"></div>
<script type="text/javascript">
var spec = {{.Get 0}}
vegaEmbed('#vis', spec);
</script>
</p>
The markdown template prior to rendering figure1's json looks like
---
title: "Aa1chart"
date: 2023-01-29T15:43:31-05:00
draft: false
---
Figure 1.
{{ '{{' }}< altairshortcode {{ figure1 }} >{{ '}}' }}
The Python code that's telling jinja to render the json into the template is (omitting the creation of the plot itself)
from pathlib import Path
from jinja2 import Template
# a scatter plot was generated with Altair and confirmed to display; omitted for brevity
p = Path.cwd()/'content/posts/aa1chart.md'
template = Template(p.read_text())
o = Path.cwd()/'content/posts/aa1chart.md'
o.write_text(template.render({
'figure1': figure1.to_json(),
}
)
)
After rendering the jinja template with the chart json, hugo runs, but hugo server -D, which launches a local server, gives the following error: unrecognized character in shortcode action: U+007B '{'. Note: Parameters with non-alphanumeric args must be quoted
After manually enclosing the json in double quotes, I got a different error unterminated quoted string in shortcode parameter-argument: '{
Some alternatives tried:
Using a named, instead of positional, parameter in the shortcode: Hugo runs, but hugo server -D errors: got quoted positional parameter. Cannot mix named and positional parameters. In the markdown, I had {{ '{{' }}< altairshortcode cjson={{ stage0 }} >{{ '}}' }}, and {{.Get "cjson"}} in the shortcode.
{{ .Inner }} converts the chart's json to a string, and so the chart does not appear in the webpage.
Placing Altair's figure1.to_html() instead of figure1.to_json() can display one chart in the webpage, but if multiple chart placeholders exist in the markdown, only one chart appears, and it is the last chart in the first position.
Looking at Hugo's built-in shortcodes, I hypothesize that if I utilized more logic or functions in my custom shortcode, such as toJSON, the json-to-string conversion can be prevented.
My GET Request:
### Bank token idclaim
GET {{IdClaimEndpoint}}/tokens/v1/id
Content-Type: application/json
> {% client.log(response.body.id-claim); %}
Response body:
{
"documentation": "http://go/proofs",
"id-claim": "eyJ0eXAiOiJKV1QiLCJhbGc"
}
Error from Response handler:
ReferenceError: "claim" is not defined. (<jsHandler script>#143)Script finished
If I try to extract value of documentation it works fine but not working for "id-claim".
I tried wrapping it within single/double quotes and also saving it to env variable and passing the env variable as {{id-claim}}, none of them worked.
It is JavaScript. You can use response.body['id-claim'] syntax. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Property_Accessors#syntax
For example, the next request works fine:
POST https://httpbin.org/post
Content-Type: application/json
{
"my-json-value": "value"
}
{%
// 'json' is a property in HTTPBin response JSON
client.log(response.body.json["my-json-value"])
%}
This works:
> {%
client.log(JSON.stringify(response.body.valueOf()).replace("\"documentation\":\"http:\/\/go\/proofs\",\"id-claim\":\"", "").replace("\"","").replace("{","").replace("}",""));
client.global.set("banktokenidclaim", JSON.stringify(response.body.valueOf()).replace("\"documentation\":\"http:\/\/go\/proofs\",\"id-claim\":\"", "").replace("\"","").replace("{","").replace("}",""));
%}
But is a lot of work for a simple thing. Not sure if there exists a easier/better solution.
In eleventy, I'm using the 'eleventy-cache-assets' utility to retrieve API data from TMDB and place it in a cache. The JSON is successfully retrieved and stored in the cache. I can also use the Nunjucks dump filter to dump the full JSON to a page. However, I cannot run a for loop over the JSON content. It's behaving as if the data does not exist. I'm likely making a schoolboy error here but I can't see it.
Here's the JS that retrieves the data (successfully).
module.exports = async function () {
try {
// Grabs either the fresh remote data or cached data (will always be fresh live)
let json = await Cache(
`https://api.themoviedb.org/3/movie/upcoming?api_key=${TOKEN}&language=en-GB®ion=GB`,
{
duration: "1d", // 1 day
type: "json",
}
);
return {
films: json,
};
} catch (e) {
return {
films: 0,
};
}
};
Here is how I'm trying to loop through the content. The else condition is returning. When I remove the else condition, nothing is returned (just the empty ul). If I've targeted the node incorrectly, I should have x number of empty li tags, but I don't.
<ul>
{% for film in films %}
<li>{{ results.title }}</li>
{% else %}
<li>This displays if the 'films' collection were empty</li>
{% endfor %}
</ul>
The issue is likely just that you're wrapping the result from the API in an additional object, and eleventy adds an additional wrapper based on the file name of the data file, so you need to modify the way you access the results.
According to the TMDB documentation, the API will return something like this (JSON-encoded):
const json = {
page: 1,
results: [
{/** ... */},
{/** ... */},
{/** ... */},
]
}
Then you're wrapping that result in another object:
return {
films: json,
};
Finally, eleventy makes that data available to templates under a variable name based on the name of your data file — i.e. the file contains your API call. You didn't mention the name of the file, let's assume it lives in your data directory as tmdb.js. That means the variable accessible to your template will look something like this:
const tmdb = {
films: {
page: 1,
results: [
{/** ... */},
{/** ... */},
{/** ... */},
]
}
}
See the documentation on how eleventy parses data files for more information.
This means you can access the results in this way:
<ul>
{% for film in tmdb.films.results %}
<li>{{ film.title }}</li>
{% else %}
<li>This displays if the 'films' collection were empty</li>
{% endfor %}
</ul>
Note also that your original code used {{ results.title }} which isn't defined in your scope, I changed that to use the loop variable film. Also, adjust the tmdb variable to the name of your data file.
I would also recommend not wrapping the response of the API in an additional object (the key films also implies it's a list of films, not the complete API response). It doesn't really add anything and only increases complexity.
I'm trying to fetch external JSON data into Jekyll and things are not working out.
I found this code (link to a gist), that is a fork of another gist... this just adds the external method ("from url").
I tried to morph that into my own tag plugin (or what ever they are called), in order to simplify it and possibly resolve some issues:
_plugins/externaljson.rb
require 'json'
require 'net/http'
module ExternalJSON
class ExternalJSON_tag < Liquid::Tag
def initialize(tag_name, text, tokens)
super
#text = text
end
def render(context)
if /(.+) from (.+)/.match(#text)
url = context[$2].strip
uri = URI( url )
response = Net::HTTP.get( uri )
data = JSON.parse( response )
context[$1] = JSON data
return ''
end
end
end
end
Liquid::Template.register_tag('externalJSON', ExternalJSON::ExternalJSON_tag)
...but I didn't really solve all my issues or learn much of anything from that. The only thing I think I learned is that the problem is likely somewhere between parsing the file in ruby and jekyll reading it.
I ran this test using the tag plugin code above(↑):
---
layout: default
---
<!-- Using the code I modified -->
<!-- This capture exists to combine a string and a variable, but it's just a static url for the purposes of this example -->
{% capture myUrl %}
https://api.guildwars2.com/v2/recipes/2889
{% endcapture %}
{% externalJSON jsonData from myUrl %}
{% for data in jsonData %}
{{ data}}
{% endfor %}
<!-- Jekyll's native way of handling local data files -->
<!-- I just saved that json data from the url above(↑) locally for this one -->
{% for data in site.data.example %}
{{ data }}
{% endfor %}
This test made me realize that both methods output the data slightly differently.
My external attempt:
{"type":"Meal","output_item_id":12210,"output_item_count":2,"min_rating":0,"time_to_craft_ms":1000,"disciplines":["Chef"],"flags":[],"ingredients":[{"item_id":24359,"count":1},{"item_id":12132,"count":1}],"id":2889,"chat_link":"[&CUkLAAA=]"}
Jekyll's native method (for local files)
{"type"=>"Meal", "output_item_id"=>12210, "output_item_count"=>2, "min_rating"=>0, "time_to_craft_ms"=>1000, "disciplines"=>["Chef"], "flags"=>[], "ingredients"=>[{"item_id"=>24359, "count"=>1}, {"item_id"=>12132, "count"=>1}], "id"=>2889, "chat_link"=>"[&CUkLAAA=]"}
And if I try to do for example {{ data.type }}, my external attempt returns nothing and the Jekyll method returns the value just like it should. I just can't figure out how to change the formatting or what ever the missing piece is there.
What am I doing wrong?
replace your render(context) with following:
def render(context)
if /(.+) from url (.+)/.match(#text)
resp = Net::HTTP.get_response(URI($2.strip))
data = resp.body
context[$1] = JSON data
nil
else
# syntax error
raise ArgumentError, 'ERROR:bad_syntax'
end
end
Then call the data like so:
{% externalJSON data from url http://foo.json %}
This will provide you with a data object that can be called to render individual keys.
If the data is an array, loop through the elements and call the desired key
{% for entry in data %}
{{ entry.type }}
{% endfor %}
If the data is an object (hash), call the key directly.
{{ data.type }}
I have three concatenated templates. base.html, menu.html, users.html. But when I execute these templates I can access data of context only from base.html.
Here is my Handler:
func HandlerUser(res http.ResponseWriter, req *http.Request){
if req.Method == "GET" {
context := Context{Title: "Users"}
users,err:= GetUser(0)
context.Data=map[string]interface{}{
"users": users,
}
fmt.Println(context.Data)
t,err := template.ParseFiles("public/base.html")
t,err = t.ParseFiles("public/menu.html")
t,err = t.ParseFiles("public/users.html")
err = t.Execute(res,context)
fmt.Println(err)
}
}
This is what I want to show in users template
{{ range $user := index .Data "users" }}
<tr id="user-{{ .Id }}">
<td id="cellId" >{{ $user.Id }}</td>
<td id="cellUserName" >{{ $user.UserName }}</td>
</tr>
{{ end }}
Note: I can access "{{.Title}}" that is used in base.html template.
First, you should check errors returned by the Template.ParseFiles() method. You store the returned error, but you only check it at the end (and by then it is overwritten like 3 times).
Next, never parse templates in the request handler, it's too time consuming and resource wasting. Do it once at startup (or on first demand). For details see It takes too much time when using "template" package to generate a dynamic web page to client in golang.
Next, you can parse multiple files at once, just enumerate all when passing to the Template.ParseFiles() function (there is a method and a function).
Know that Template.Execute() only executes a single (named) template. You have 3 associated templates, but only the "base.html" template is executed by your code. To execute a specific, named template, use Template.ExecuteTemplate(). For details, see Telling Golang which template to execute first.
First you should define a structure of your templates, decide which templates include others, and execute the "wrapper" template (using Template.ExecuteTemplate()). When you execute a template that invokes / includes another template, you have the possibility to tell what value (data) you what to pass to its execution. When you write {{template "something" .}}, that means you want to pass the value currently pointed by dot to the execution of the template named "something". Read more about this: golang template engine pipelines.
To learn more about template association and internals, read this answer: Go template name.
So in your case I would imagine that "base.html" is the wrapper, outer template, which includes "menu.html" and "users.html". So "base.html" should contain lines similar to this:
{{template "menu.html" .}}
{{template "users.html" .}}
The above lines will invoke and include the results of the mentioned templates, passing the data to their execution that was passed to "base.html" (if dot was not changed).
Parse the files using the template.ParseFiles() function (not method) like this:
var t *template.Template
func init() {
var err error
t, err = template.ParseFiles(
"public/base.html", "public/menu.html", "public/users.html")
if err != nil {
panic(err) // handle error
}
}
And execute it like this in your handler:
err := t.ExecuteTemplate(w, "base.html", context)